Merge pull request #231 from Hy4ri/refactor/issue-78-strings-extraction

refactor(#78): extract hardcoded strings to strings.xml
This commit is contained in:
M57 authored and GitHub committed 2026-06-21 12:23:10 +03:00
commit d1d5b0937c
32 files changed
+906 -362

No files matched your search

@@ -1,5 +1,6 @@
package com.m57.hermescontrol
import androidx.annotation.StringRes
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
@@ -58,6 +59,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.navigation3.runtime.NavKey
@@ -94,33 +96,33 @@ import com.m57.hermescontrol.ui.webhooks.WebhooksScreen as WebhooksScreenContent
private data class BottomNavItem(
val key: NavKey,
val label: String,
@param:StringRes val labelRes: Int,
val icon: ImageVector,
)
/** Master list of ALL screens available for bottom-nav selection. */
private val ALL_NAV_ITEMS: List<BottomNavItem> =
listOf(
BottomNavItem(ChatScreen, "Chat", Icons.AutoMirrored.Filled.Chat),
BottomNavItem(SkillsScreen, "Skills", Icons.Filled.Extension),
BottomNavItem(CronJobsScreen, "Cron", Icons.Filled.Schedule),
BottomNavItem(SystemScreen, "System", Icons.Filled.Info),
BottomNavItem(SettingsScreen, "Settings", Icons.Filled.Settings),
BottomNavItem(ProfilesScreen, "Profiles", Icons.Filled.AccountCircle),
BottomNavItem(WebhooksScreen, "Webhooks", Icons.Filled.Webhook),
BottomNavItem(GatewayScreen, "Gateway", Icons.Filled.Bolt),
BottomNavItem(ToolsetsScreen, "Toolsets", Icons.Filled.Build),
BottomNavItem(PluginsScreen, "Plugins", Icons.Filled.Memory),
BottomNavItem(ConfigScreen, "Config", Icons.Filled.Code),
BottomNavItem(McpServersScreen, "MCP Servers", Icons.Filled.Dashboard),
BottomNavItem(ModelScreen, "Models", Icons.Filled.Settings),
BottomNavItem(PairingScreen, "Pairing", Icons.Filled.Devices),
BottomNavItem(KeysScreen, "Keys", Icons.Filled.Key),
BottomNavItem(ChannelsScreen, "Channels", Icons.AutoMirrored.Filled.ListAlt),
BottomNavItem(LogsScreen, "Logs", Icons.Filled.HistoryEdu),
BottomNavItem(KanbanScreen, "Kanban", Icons.Filled.Dashboard),
BottomNavItem(AchievementsScreen, "Achievements", Icons.Filled.Info),
BottomNavItem(HistoryScreen, "History", Icons.Filled.History),
BottomNavItem(ChatScreen, R.string.screen_chat, Icons.AutoMirrored.Filled.Chat),
BottomNavItem(SkillsScreen, R.string.screen_skills, Icons.Filled.Extension),
BottomNavItem(CronJobsScreen, R.string.screen_cron, Icons.Filled.Schedule),
BottomNavItem(SystemScreen, R.string.screen_system, Icons.Filled.Info),
BottomNavItem(SettingsScreen, R.string.screen_settings, Icons.Filled.Settings),
BottomNavItem(ProfilesScreen, R.string.screen_profiles, Icons.Filled.AccountCircle),
BottomNavItem(WebhooksScreen, R.string.screen_webhooks, Icons.Filled.Webhook),
BottomNavItem(GatewayScreen, R.string.screen_gateway, Icons.Filled.Bolt),
BottomNavItem(ToolsetsScreen, R.string.screen_toolsets, Icons.Filled.Build),
BottomNavItem(PluginsScreen, R.string.screen_plugins, Icons.Filled.Memory),
BottomNavItem(ConfigScreen, R.string.screen_config, Icons.Filled.Code),
BottomNavItem(McpServersScreen, R.string.screen_mcp_servers, Icons.Filled.Dashboard),
BottomNavItem(ModelScreen, R.string.screen_models, Icons.Filled.Settings),
BottomNavItem(PairingScreen, R.string.screen_pairing, Icons.Filled.Devices),
BottomNavItem(KeysScreen, R.string.screen_keys, Icons.Filled.Key),
BottomNavItem(ChannelsScreen, R.string.screen_channels, Icons.AutoMirrored.Filled.ListAlt),
BottomNavItem(LogsScreen, R.string.screen_logs, Icons.Filled.HistoryEdu),
BottomNavItem(KanbanScreen, R.string.screen_kanban, Icons.Filled.Dashboard),
BottomNavItem(AchievementsScreen, R.string.screen_achievements, Icons.Filled.Info),
BottomNavItem(HistoryScreen, R.string.screen_history, Icons.Filled.History),
)
/** Lookup: data-object simple name → NavKey (used by bottom-nav config). */
@@ -134,17 +136,17 @@ private fun resolveBottomNavItems(names: List<String>): List<BottomNavItem> =
// ── Drawer sections ────────────────────────────────────────────────────
private enum class DrawerSection(
val title: String,
@param:StringRes val titleRes: Int,
) {
CONVERSE("Converse"),
AUTOMATE("Automate"),
CONFIGURE("Configure"),
INSPECT("Inspect"),
CONVERSE(R.string.nav_drawer_section_converse),
AUTOMATE(R.string.nav_drawer_section_automate),
CONFIGURE(R.string.nav_drawer_section_configure),
INSPECT(R.string.nav_drawer_section_inspect),
}
private data class DrawerEntry(
val key: NavKey,
val label: String,
@param:StringRes val labelRes: Int,
val icon: ImageVector,
val section: DrawerSection,
)
@@ -152,29 +154,34 @@ private data class DrawerEntry(
private val DRAWER_ENTRIES =
listOf(
// Converse
DrawerEntry(ChatScreen, "Chat", Icons.AutoMirrored.Filled.Chat, DrawerSection.CONVERSE),
DrawerEntry(HistoryScreen, "History", Icons.Filled.History, DrawerSection.CONVERSE),
DrawerEntry(ProfilesScreen, "Profiles", Icons.Filled.AccountCircle, DrawerSection.CONVERSE),
DrawerEntry(ChatScreen, R.string.screen_chat, Icons.AutoMirrored.Filled.Chat, DrawerSection.CONVERSE),
DrawerEntry(HistoryScreen, R.string.screen_history, Icons.Filled.History, DrawerSection.CONVERSE),
DrawerEntry(ProfilesScreen, R.string.screen_profiles, Icons.Filled.AccountCircle, DrawerSection.CONVERSE),
// Automate
DrawerEntry(CronJobsScreen, "Cron", Icons.Filled.Schedule, DrawerSection.AUTOMATE),
DrawerEntry(WebhooksScreen, "Webhooks", Icons.Filled.Webhook, DrawerSection.AUTOMATE),
DrawerEntry(GatewayScreen, "Gateway", Icons.Filled.Bolt, DrawerSection.AUTOMATE),
DrawerEntry(CronJobsScreen, R.string.screen_cron, Icons.Filled.Schedule, DrawerSection.AUTOMATE),
DrawerEntry(WebhooksScreen, R.string.screen_webhooks, Icons.Filled.Webhook, DrawerSection.AUTOMATE),
DrawerEntry(GatewayScreen, R.string.screen_gateway, Icons.Filled.Bolt, DrawerSection.AUTOMATE),
// Configure
DrawerEntry(SkillsScreen, "Skills", Icons.Filled.Extension, DrawerSection.CONFIGURE),
DrawerEntry(SettingsScreen, "Settings", Icons.Filled.Settings, DrawerSection.CONFIGURE),
DrawerEntry(ToolsetsScreen, "Toolsets", Icons.Filled.Build, DrawerSection.CONFIGURE),
DrawerEntry(PluginsScreen, "Plugins", Icons.Filled.Memory, DrawerSection.CONFIGURE),
DrawerEntry(ConfigScreen, "Config", Icons.Filled.Code, DrawerSection.CONFIGURE),
DrawerEntry(McpServersScreen, "MCP Servers", Icons.Filled.Dashboard, DrawerSection.CONFIGURE),
DrawerEntry(ModelScreen, "Models", Icons.Filled.Settings, DrawerSection.CONFIGURE),
DrawerEntry(PairingScreen, "Pairing", Icons.Filled.Devices, DrawerSection.CONFIGURE),
DrawerEntry(KeysScreen, "Keys", Icons.Filled.Key, DrawerSection.CONFIGURE),
DrawerEntry(ChannelsScreen, "Channels", Icons.AutoMirrored.Filled.ListAlt, DrawerSection.CONFIGURE),
DrawerEntry(SkillsScreen, R.string.screen_skills, Icons.Filled.Extension, DrawerSection.CONFIGURE),
DrawerEntry(SettingsScreen, R.string.screen_settings, Icons.Filled.Settings, DrawerSection.CONFIGURE),
DrawerEntry(ToolsetsScreen, R.string.screen_toolsets, Icons.Filled.Build, DrawerSection.CONFIGURE),
DrawerEntry(PluginsScreen, R.string.screen_plugins, Icons.Filled.Memory, DrawerSection.CONFIGURE),
DrawerEntry(ConfigScreen, R.string.screen_config, Icons.Filled.Code, DrawerSection.CONFIGURE),
DrawerEntry(McpServersScreen, R.string.screen_mcp_servers, Icons.Filled.Dashboard, DrawerSection.CONFIGURE),
DrawerEntry(ModelScreen, R.string.screen_models, Icons.Filled.Settings, DrawerSection.CONFIGURE),
DrawerEntry(PairingScreen, R.string.screen_pairing, Icons.Filled.Devices, DrawerSection.CONFIGURE),
DrawerEntry(KeysScreen, R.string.screen_keys, Icons.Filled.Key, DrawerSection.CONFIGURE),
DrawerEntry(
ChannelsScreen,
R.string.screen_channels,
Icons.AutoMirrored.Filled.ListAlt,
DrawerSection.CONFIGURE,
),
// Inspect
DrawerEntry(SystemScreen, "System", Icons.Filled.Info, DrawerSection.INSPECT),
DrawerEntry(LogsScreen, "Logs", Icons.Filled.HistoryEdu, DrawerSection.INSPECT),
DrawerEntry(KanbanScreen, "Kanban", Icons.Filled.Dashboard, DrawerSection.INSPECT),
DrawerEntry(AchievementsScreen, "Achievements", Icons.Filled.Info, DrawerSection.INSPECT),
DrawerEntry(SystemScreen, R.string.screen_system, Icons.Filled.Info, DrawerSection.INSPECT),
DrawerEntry(LogsScreen, R.string.screen_logs, Icons.Filled.HistoryEdu, DrawerSection.INSPECT),
DrawerEntry(KanbanScreen, R.string.screen_kanban, Icons.Filled.Dashboard, DrawerSection.INSPECT),
DrawerEntry(AchievementsScreen, R.string.screen_achievements, Icons.Filled.Info, DrawerSection.INSPECT),
)
private val DRAWER_GESTURE_SCREENS: Set<NavKey> = ALL_NAV_ITEMS.mapTo(mutableSetOf()) { it.key }
@@ -370,7 +377,7 @@ fun MainNavigation(sessionId: String? = null) {
modifier = Modifier.padding(start = 20.dp, top = 20.dp, end = 16.dp, bottom = 4.dp),
) {
Text(
text = "⚡ Hermes",
text = stringResource(R.string.nav_drawer_title),
style =
MaterialTheme.typography.headlineSmall.copy(
fontWeight = FontWeight.Bold,
@@ -386,7 +393,7 @@ fun MainNavigation(sessionId: String? = null) {
)
}
Text(
text = "AI Agent Control",
text = stringResource(R.string.nav_drawer_subtitle),
modifier = Modifier.padding(start = 20.dp, bottom = 12.dp, end = 16.dp),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -395,7 +402,7 @@ fun MainNavigation(sessionId: String? = null) {
for (section in DrawerSection.entries) {
Text(
text = section.title.uppercase(),
text = stringResource(section.titleRes).uppercase(),
modifier =
Modifier.padding(
start = 20.dp,
@@ -412,7 +419,7 @@ fun MainNavigation(sessionId: String? = null) {
.forEach { entry ->
NavigationDrawerItem(
icon = { Icon(entry.icon, contentDescription = null) },
label = { Text(entry.label) },
label = { Text(stringResource(entry.labelRes)) },
selected = currentScreen == entry.key,
onClick = {
scope.launch { drawerState.close() }
@@ -427,7 +434,11 @@ fun MainNavigation(sessionId: String? = null) {
modifier =
Modifier
.padding(horizontal = 12.dp, vertical = 2.dp)
.testTag("drawer_${entry.label.lowercase().replace(' ', '_')}"),
.testTag(
"drawer_${entry.key::class.simpleName?.lowercase()?.removeSuffix(
"screen",
) ?: ""}",
),
)
}
}
@@ -448,9 +459,12 @@ fun MainNavigation(sessionId: String? = null) {
NavigationBarItem(
selected = currentScreen == item.key,
onClick = { NavigationController.navigateTo(item.key) },
icon = { Icon(item.icon, contentDescription = item.label) },
label = { Text(item.label) },
modifier = Modifier.testTag("nav_${item.label.lowercase().replace(' ', '_')}"),
icon = { Icon(item.icon, contentDescription = stringResource(item.labelRes)) },
label = { Text(stringResource(item.labelRes)) },
modifier =
Modifier.testTag(
"nav_${item.key::class.simpleName?.lowercase()?.removeSuffix("screen") ?: ""}",
),
)
}
}
@@ -95,9 +95,7 @@ object AuthManager {
requirePrefs().edit().putString(KEY_CONNECTION_PROFILES, json).apply()
}
fun getProfileToken(profileId: String): String? {
return requirePrefs().getString("token_$profileId", null)
}
fun getProfileToken(profileId: String): String? = requirePrefs().getString("token_$profileId", null)
fun setProfileToken(
profileId: String,
@@ -70,7 +70,7 @@ class ChatNotificationService : Service() {
override fun onCreate() {
super.onCreate()
createNotificationChannels()
startForeground(NOTIFICATION_ID, buildForegroundNotification("Waiting for Hermes replies"))
startForeground(NOTIFICATION_ID, buildForegroundNotification(getString(R.string.notif_waiting_replies)))
startEventCollection()
}
@@ -85,12 +85,12 @@ class ChatNotificationService : Service() {
event.text
.take(100)
.replace("\n", " ")
.ifBlank { "New message" }
.ifBlank { getString(R.string.notif_new_message) }
showReplyNotification(preview, event.sessionId)
}
is WsEvent.ClarifyRequest -> {
showReplyNotification("Hermes needs a clarification", null)
showReplyNotification(getString(R.string.notif_clarification_needed), null)
}
else -> {}
@@ -108,7 +108,7 @@ class ChatNotificationService : Service() {
NotificationCompat
.Builder(this, CHAT_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Hermes")
.setContentTitle(getString(R.string.notif_title))
.setContentText(text)
.setStyle(NotificationCompat.BigTextStyle().bigText(text))
.setPriority(NotificationCompat.PRIORITY_HIGH)
@@ -116,7 +116,7 @@ class ChatNotificationService : Service() {
.setContentIntent(buildContentIntent(sessionId))
if (!sessionId.isNullOrBlank()) {
val replyLabel = "Type your reply..."
val replyLabel = getString(R.string.notif_reply_placeholder)
val remoteInput =
RemoteInput
.Builder(NotificationReplyReceiver.KEY_TEXT_REPLY)
@@ -140,7 +140,7 @@ class ChatNotificationService : Service() {
NotificationCompat.Action
.Builder(
R.drawable.ic_notification,
"Reply",
getString(R.string.action_reply),
replyPendingIntent,
).addRemoteInput(remoteInput)
.build()
@@ -169,7 +169,7 @@ class ChatNotificationService : Service() {
NotificationCompat
.Builder(this, SERVICE_CHANNEL_ID)
.setSmallIcon(R.drawable.ic_notification)
.setContentTitle("Hermes")
.setContentTitle(getString(R.string.notif_title))
.setContentText(text)
.setPriority(NotificationCompat.PRIORITY_MIN)
.setOngoing(true)
@@ -180,19 +180,19 @@ class ChatNotificationService : Service() {
val serviceChannel =
NotificationChannel(
SERVICE_CHANNEL_ID,
"Hermes Background Service",
getString(R.string.notif_channel_service_name),
NotificationManager.IMPORTANCE_MIN,
).apply {
description = "Keeps connection alive in the background"
description = getString(R.string.notif_channel_service_desc)
}
val chatChannel =
NotificationChannel(
CHAT_CHANNEL_ID,
"Hermes Chat Replies",
getString(R.string.notif_channel_chat_name),
NotificationManager.IMPORTANCE_HIGH,
).apply {
description = "Alerts for new replies from Hermes"
description = getString(R.string.notif_channel_chat_desc)
}
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
@@ -23,10 +23,12 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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 androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -45,7 +47,7 @@ fun AchievementsScreen(
}
HermesScaffold(
title = { Text("Agent Achievements") },
title = { Text(stringResource(R.string.screen_achievements)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadAchievements() },
@@ -65,8 +67,8 @@ fun AchievementsScreen(
state.achievements.isEmpty() -> {
EmptyState(
title = "No achievements yet",
subtitle = "Complete tasks to unlock achievements.",
title = stringResource(R.string.achievements_empty_title),
subtitle = stringResource(R.string.achievements_empty_desc),
icon = Icons.Filled.Refresh,
modifier = Modifier.padding(paddingValues),
)
@@ -156,8 +158,11 @@ fun AchievementsScreen(
) {
Text(
text =
"Progress: ${achievement.progress?.toInt() ?: 0} / " +
"${achievement.next_threshold?.toInt() ?: 0}",
stringResource(
R.string.achievements_label_progress,
achievement.progress?.toInt() ?: 0,
achievement.next_threshold?.toInt() ?: 0,
),
style = MaterialTheme.typography.bodySmall,
)
Text(
@@ -29,9 +29,11 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.MessagingPlatformUpdate
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
@@ -59,7 +61,7 @@ fun ChannelsScreen(
}
HermesScaffold(
title = { Text("Messaging Channels") },
title = { Text(stringResource(R.string.screen_channels)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadPlatforms() },
@@ -79,8 +81,8 @@ fun ChannelsScreen(
state.platforms.isEmpty() -> {
EmptyState(
title = "No channels configured",
subtitle = "Add messaging platforms in Hermes to see them here.",
title = stringResource(R.string.channels_empty_title),
subtitle = stringResource(R.string.channels_empty_desc),
modifier = Modifier.padding(paddingValues),
)
}
@@ -104,7 +106,14 @@ fun ChannelsScreen(
Column {
Text(text = platform.name, style = MaterialTheme.typography.titleLarge)
Text(
text = if (platform.enabled) "Active" else "Disabled",
text =
if (platform.enabled) {
stringResource(
R.string.channels_status_active,
)
} else {
stringResource(R.string.channels_status_disabled)
},
color =
if (platform.enabled) {
MaterialTheme.colorScheme.primary
@@ -126,7 +135,15 @@ fun ChannelsScreen(
)
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = { showConfigureForm = !showConfigureForm }) {
Text(if (showConfigureForm) "Cancel" else "Configure")
Text(
if (showConfigureForm) {
stringResource(
R.string.action_cancel,
)
} else {
stringResource(R.string.channels_action_configure)
},
)
}
}
}
@@ -134,7 +151,7 @@ fun ChannelsScreen(
if (showConfigureForm) {
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Configuration Settings",
text = stringResource(R.string.channels_sec_settings),
style = MaterialTheme.typography.titleMedium,
)
@@ -148,7 +165,7 @@ fun ChannelsScreen(
label = { Text(field.prompt ?: field.key) },
placeholder = {
if (field.isSet) {
Text("******** (Already Configured)")
Text(stringResource(R.string.channels_placeholder_configured))
}
},
modifier = Modifier.fillMaxWidth(),
@@ -169,13 +186,16 @@ fun ChannelsScreen(
},
modifier = Modifier.align(Alignment.End),
) {
Text("Save Settings")
Text(stringResource(R.string.channels_action_save_settings))
}
} else {
val envFields = platform.envVars.orEmpty()
if (envFields.isNotEmpty()) {
Spacer(modifier = Modifier.height(8.dp))
Text(text = "Configured Keys:", style = MaterialTheme.typography.titleSmall)
Text(
text = stringResource(R.string.channels_sec_keys),
style = MaterialTheme.typography.titleSmall,
)
envFields.forEach { field ->
Row(
modifier = Modifier.fillMaxWidth(),
@@ -187,7 +207,14 @@ fun ChannelsScreen(
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
Text(
text = if (field.isSet) "Configured" else "Not set",
text =
if (field.isSet) {
stringResource(
R.string.channels_status_configured,
)
} else {
stringResource(R.string.channels_status_not_set)
},
style = MaterialTheme.typography.bodyMedium,
color =
if (field.isSet) {
@@ -54,6 +54,7 @@ import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalClipboardManager
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.AnnotatedString
import androidx.compose.ui.text.SpanStyle
import androidx.compose.ui.text.buildAnnotatedString
@@ -64,6 +65,7 @@ import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.text.withStyle
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.AssistantBubble
import com.m57.hermescontrol.theme.AssistantBubbleLight
import com.m57.hermescontrol.theme.StatusGreen
@@ -232,7 +234,7 @@ private fun UserBubble(
) {
Icon(
Icons.Filled.ContentCopy,
contentDescription = "Copy",
contentDescription = stringResource(R.string.content_desc_copy),
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
@@ -347,7 +349,7 @@ private fun AssistantBubble(
) {
Icon(
Icons.Filled.ContentCopy,
contentDescription = "Copy",
contentDescription = stringResource(R.string.content_desc_copy),
modifier = Modifier.size(16.dp),
tint = MaterialTheme.colorScheme.onSurface,
)
@@ -489,7 +491,7 @@ private fun ParsedToolContent(
}
parsed.stderr?.let {
Text(
text = "Error Output (stderr):\n$it",
text = stringResource(R.string.chat_tool_stderr, it),
style =
MaterialTheme.typography.bodySmall.copy(
color = StatusRed.copy(alpha = 0.9f),
@@ -500,7 +502,7 @@ private fun ParsedToolContent(
}
parsed.error?.let {
Text(
text = "Execution Error: $it",
text = stringResource(R.string.chat_tool_execution_error, it),
style =
MaterialTheme.typography.bodySmall.copy(
color = StatusRed,
@@ -513,7 +515,7 @@ private fun ParsedToolContent(
parsed.exitCode?.let { code ->
if (code != 0) {
Text(
text = "Exit Code: $code",
text = stringResource(R.string.chat_tool_exit_code, code),
style =
MaterialTheme.typography.labelSmall.copy(
color = StatusRed,
@@ -619,7 +621,7 @@ private fun ToolBubble(
}
Text(
text = message.toolName ?: "Tool",
text = message.toolName ?: stringResource(R.string.chat_tool_fallback),
style =
MaterialTheme.typography.labelMedium.copy(
color = contentColor,
@@ -635,7 +637,7 @@ private fun ToolBubble(
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Show Raw JSON",
text = stringResource(R.string.chat_tool_show_raw),
style =
MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.primary,
@@ -660,7 +662,7 @@ private fun ToolBubble(
if (parsedOutput != null) {
Spacer(modifier = Modifier.height(8.dp))
Text(
text = "Show Parsed Output",
text = stringResource(R.string.chat_tool_show_parsed),
style =
MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.primary,
@@ -293,14 +293,20 @@ fun ChatScreen(
imageVector =
if (state.isSearchActive) Icons.Filled.Close else Icons.Filled.Search,
contentDescription =
if (state.isSearchActive) "Close search" else "Search",
if (state.isSearchActive) {
stringResource(
R.string.chat_action_close_search,
)
} else {
stringResource(R.string.chat_action_search)
},
)
}
IconButton(onClick = { viewModel.createNewSession() }) {
Icon(
imageVector = Icons.Filled.Add,
contentDescription = "New Chat",
contentDescription = stringResource(R.string.content_desc_new_chat),
)
}
},
@@ -335,9 +341,9 @@ fun ChatScreen(
Text(
text =
if (state.connectionStatus == ConnectionStatus.RECONNECTING) {
"Reconnecting to Hermes…"
stringResource(R.string.chat_status_reconnecting)
} else {
"Disconnected from Hermes"
stringResource(R.string.chat_status_disconnected)
},
style = MaterialTheme.typography.bodyMedium,
)
@@ -349,7 +355,7 @@ fun ChatScreen(
contentColor = MaterialTheme.colorScheme.onErrorContainer,
),
) {
Text("Reconnect")
Text(stringResource(R.string.chat_action_reconnect))
}
}
}
@@ -393,8 +399,8 @@ fun ChatScreen(
) {
if (state.messages.isEmpty() && !state.isLoading) {
EmptyState(
title = "Ready to chat",
subtitle = "Send a message to start a conversation",
title = stringResource(R.string.chat_empty_title),
subtitle = stringResource(R.string.chat_empty_subtitle),
)
}
@@ -482,7 +488,7 @@ fun ChatScreen(
)
Spacer(modifier = Modifier.height(16.dp))
Text(
text = "Connecting to Hermes…",
text = stringResource(R.string.chat_status_connecting),
style = MaterialTheme.typography.bodyMedium,
)
}
@@ -512,7 +518,7 @@ fun ChatScreen(
) {
Icon(
imageVector = Icons.Filled.KeyboardArrowDown,
contentDescription = "Scroll to bottom",
contentDescription = stringResource(R.string.content_desc_scroll_to_bottom),
)
}
}
@@ -584,9 +590,9 @@ private fun ThinkingIndicator(thinkingText: String) {
text =
if (thinkingText.isNotBlank()) {
val display = thinkingText.takeLast(100)
"Thinking: $display…"
stringResource(R.string.chat_thinking_param, display)
} else {
"Thinking…"
stringResource(R.string.chat_thinking)
},
style =
MaterialTheme.typography.bodySmall.copy(
@@ -671,14 +677,14 @@ private fun ChatInputBar(
items(filteredCommands) { cmd ->
val description =
when (cmd) {
"/help" -> "Show help menu"
"/status" -> "Check gateway and platform status"
"/sessions" -> "List all chat sessions"
"/stats" -> "Check system resource usage (stats)"
"/system" -> "Check system resource usage (system)"
"/new" -> "Create a new chat session"
"/stop" -> "Interrupt the active run"
"/interrupt" -> "Interrupt the active run"
"/help" -> stringResource(R.string.command_help_desc)
"/status" -> stringResource(R.string.command_status_desc)
"/sessions" -> stringResource(R.string.command_sessions_desc)
"/stats" -> stringResource(R.string.command_stats_desc)
"/system" -> stringResource(R.string.command_system_desc)
"/new" -> stringResource(R.string.command_new_desc)
"/stop" -> stringResource(R.string.command_stop_desc)
"/interrupt" -> stringResource(R.string.command_stop_desc)
else -> ""
}
DropdownMenuItem(
@@ -725,11 +731,11 @@ private fun ChatInputBar(
placeholder = {
Text(
if (!isConnected) {
"Not connected…"
stringResource(R.string.chat_input_placeholder_not_connected)
} else if (isAgentTyping) {
"Waiting for response…"
stringResource(R.string.chat_input_placeholder_waiting)
} else {
"Type a message…"
stringResource(R.string.chat_input_placeholder_type_message)
},
)
},
@@ -759,7 +765,7 @@ private fun ChatInputBar(
) {
Icon(
imageVector = Icons.Filled.Stop,
contentDescription = "Interrupt",
contentDescription = stringResource(R.string.content_desc_interrupt),
tint = MaterialTheme.colorScheme.error,
)
}
@@ -150,13 +150,33 @@ class ChatViewModel(
is WsEvent.SessionInfo -> {}
is WsEvent.MessageStart -> handleMessageStart(event)
is WsEvent.MessageToken -> handleMessageToken(event)
is WsEvent.ThinkingDelta -> handleThinkingDelta(event)
is WsEvent.MessageComplete -> handleMessageComplete(event)
is WsEvent.MessageDone -> handleMessageDone(event)
is WsEvent.ToolStart -> handleToolStart(event)
is WsEvent.ToolComplete -> handleToolComplete(event)
is WsEvent.MessageStart -> {
handleMessageStart(event)
}
is WsEvent.MessageToken -> {
handleMessageToken(event)
}
is WsEvent.ThinkingDelta -> {
handleThinkingDelta(event)
}
is WsEvent.MessageComplete -> {
handleMessageComplete(event)
}
is WsEvent.MessageDone -> {
handleMessageDone(event)
}
is WsEvent.ToolStart -> {
handleToolStart(event)
}
is WsEvent.ToolComplete -> {
handleToolComplete(event)
}
is WsEvent.ClarifyRequest -> {
if (!isCurrentSession(event.sessionId)) return
@@ -21,6 +21,8 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import com.m57.hermescontrol.R
/**
* Shared Scaffold wrapper that standardizes the TopAppBar.
@@ -69,7 +71,7 @@ fun HermesScaffold(
IconButton(onClick = onBack) {
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = "Back",
contentDescription = stringResource(R.string.content_desc_back),
modifier = Modifier.testTag("back_button"),
)
}
@@ -79,7 +81,7 @@ fun HermesScaffold(
IconButton(onClick = onOpenDrawer) {
Icon(
imageVector = Icons.Filled.Menu,
contentDescription = "Open navigation drawer",
contentDescription = stringResource(R.string.content_desc_open_drawer),
modifier = Modifier.testTag("menu_button"),
)
}
@@ -91,7 +93,7 @@ fun HermesScaffold(
IconButton(onClick = onRefresh) {
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = "Refresh",
contentDescription = stringResource(R.string.content_desc_refresh),
modifier = Modifier.testTag("refresh_button"),
)
}
@@ -31,8 +31,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
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.theme.LocalHermesStatusColors
import com.m57.hermescontrol.theme.LocalSpacing
@@ -215,7 +217,7 @@ fun SearchBar(
IconButton(onClick = { onQueryChange("") }) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Clear search",
contentDescription = stringResource(R.string.content_desc_clear_search),
)
}
}
@@ -248,7 +250,15 @@ fun FilterChipRow(
FilterChip(
selected = chip == selectedChip,
onClick = { onChipSelected(chip) },
label = { Text(chip) },
label = {
Text(
if (chip == "All") {
stringResource(R.string.skills_category_all)
} else {
chip
},
)
},
)
}
}
@@ -23,8 +23,10 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.LocalSpacing
// Unified loading / error / empty / skeleton placeholders so every screen
@@ -94,7 +96,7 @@ fun ErrorState(
modifier = Modifier.size(18.dp),
)
Spacer(modifier = Modifier.size(spacing.sm))
Text("Retry")
Text(stringResource(R.string.action_retry))
}
}
}
@@ -26,10 +26,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
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.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState
@@ -57,7 +59,7 @@ fun ConfigScreen(
}
HermesScaffold(
title = { Text("Configuration") },
title = { Text(stringResource(R.string.config_screen_title)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadRawConfig() },
@@ -87,7 +89,7 @@ fun ConfigScreen(
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.loadRawConfig() }) {
Text("Retry")
Text(stringResource(R.string.action_retry))
}
}
} else {
@@ -113,7 +115,7 @@ fun ConfigScreen(
Modifier
.fillMaxWidth()
.weight(1f),
placeholder = { Text("YAML Config...") },
placeholder = { Text(stringResource(R.string.config_placeholder_yaml)) },
textStyle = MaterialTheme.typography.bodyMedium.copy(fontFamily = FontFamily.Monospace),
maxLines = Int.MAX_VALUE,
)
@@ -125,7 +127,7 @@ fun ConfigScreen(
onClick = { editingText = state.yamlText ?: "" },
modifier = Modifier.weight(1f),
) {
Text("Reset")
Text(stringResource(R.string.config_action_reset))
}
Spacer(modifier = Modifier.width(16.dp))
@@ -138,7 +140,7 @@ fun ConfigScreen(
if (state.isSaving) {
CircularProgressIndicator(modifier = Modifier.width(16.dp))
} else {
Text("Save Config")
Text(stringResource(R.string.config_action_save))
}
}
}
@@ -45,6 +45,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -53,6 +54,7 @@ import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
@Composable
fun ConnectScreen(
@@ -106,7 +108,7 @@ fun ConnectScreen(
}
Text(
text = "Hermes",
text = stringResource(R.string.notif_title),
style =
MaterialTheme.typography.displayMedium.copy(
fontWeight = FontWeight.Bold,
@@ -114,7 +116,7 @@ fun ConnectScreen(
),
)
Text(
text = "AI Agent Control",
text = stringResource(R.string.nav_drawer_subtitle),
style =
MaterialTheme.typography.bodyLarge.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -134,9 +136,9 @@ fun ConnectScreen(
Text(
text =
if (state.selectedProfile != null) {
"Profile: ${state.selectedProfile!!.name}"
stringResource(R.string.connect_selected_profile, state.selectedProfile!!.name)
} else {
"Select Saved Profile"
stringResource(R.string.connect_action_select_profile)
},
)
}
@@ -146,7 +148,7 @@ fun ConnectScreen(
modifier = Modifier.fillMaxWidth(0.85f),
) {
DropdownMenuItem(
text = { Text("None (Standalone)") },
text = { Text(stringResource(R.string.connect_profile_none)) },
onClick = {
viewModel.selectProfile(null)
profilesExpanded = false
@@ -171,7 +173,7 @@ fun ConnectScreen(
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = "Pairing string",
text = stringResource(R.string.connect_pairing_string),
style = MaterialTheme.typography.labelLarge,
color = MaterialTheme.colorScheme.onBackground,
)
@@ -182,7 +184,7 @@ fun ConnectScreen(
pairingString = it
if (it.isNotBlank()) viewModel.onPairingString(it)
},
placeholder = { Text("Paste hermes://… or Base64 config") },
placeholder = { Text(stringResource(R.string.connect_placeholder_pairing)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
@@ -215,7 +217,7 @@ fun ConnectScreen(
onCheckedChange = viewModel::onSaveProfileChange,
)
Text(
text = "Save connection profile",
text = stringResource(R.string.connect_save_profile),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onBackground,
)
@@ -225,8 +227,8 @@ fun ConnectScreen(
OutlinedTextField(
value = state.profileName,
onValueChange = viewModel::onProfileNameChange,
label = { Text("Profile Name") },
placeholder = { Text("e.g. Local Hermit, Remote Server") },
label = { Text(stringResource(R.string.connect_profile_name)) },
placeholder = { Text(stringResource(R.string.connect_placeholder_profile_name)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
@@ -247,7 +249,7 @@ fun ConnectScreen(
strokeWidth = 2.dp,
)
} else {
Text("Connect", style = MaterialTheme.typography.labelLarge)
Text(stringResource(R.string.action_connect), style = MaterialTheme.typography.labelLarge)
}
}
@@ -256,7 +258,12 @@ fun ConnectScreen(
modifier = Modifier.fillMaxWidth(),
) {
Text(
text = if (showManualFields) "Hide manual entry" else "Enter manually instead",
text =
if (showManualFields) {
stringResource(R.string.connect_action_hide_manual)
} else {
stringResource(R.string.connect_action_show_manual)
},
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
)
@@ -271,8 +278,8 @@ fun ConnectScreen(
OutlinedTextField(
value = state.token,
onValueChange = viewModel::onTokenChange,
label = { Text("Auth Token") },
placeholder = { Text("API token") },
label = { Text(stringResource(R.string.settings_field_token)) },
placeholder = { Text(stringResource(R.string.connect_placeholder_token)) },
singleLine = true,
visualTransformation =
if (passwordVisible) {
@@ -290,7 +297,11 @@ fun ConnectScreen(
Icons.Filled.VisibilityOff
},
contentDescription =
if (passwordVisible) "Hide token" else "Show token",
if (passwordVisible) {
stringResource(R.string.content_desc_hide_token)
} else {
stringResource(R.string.content_desc_show_token)
},
)
}
},
@@ -299,16 +310,16 @@ fun ConnectScreen(
OutlinedTextField(
value = state.host,
onValueChange = viewModel::onHostChange,
label = { Text("Host") },
placeholder = { Text("127.0.0.1") },
label = { Text(stringResource(R.string.settings_field_host)) },
placeholder = { Text(stringResource(R.string.connect_placeholder_host)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
)
OutlinedTextField(
value = state.port,
onValueChange = viewModel::onPortChange,
label = { Text("Port") },
placeholder = { Text("9119") },
label = { Text(stringResource(R.string.settings_field_port)) },
placeholder = { Text(stringResource(R.string.connect_placeholder_port)) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
@@ -24,9 +24,11 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontWeight
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
@@ -60,7 +62,7 @@ fun CronJobsScreen(
}
HermesScaffold(
title = { Text("Cron Jobs") },
title = { Text(stringResource(R.string.screen_cron)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadCronJobs() },
@@ -79,8 +81,8 @@ fun CronJobsScreen(
state.jobs.isEmpty() -> {
EmptyState(
title = "No cron jobs",
subtitle = "Scheduled tasks from Hermes will appear here.",
title = stringResource(R.string.cron_empty_title),
subtitle = stringResource(R.string.cron_empty_desc),
icon = Icons.Filled.Schedule,
)
}
@@ -112,7 +114,14 @@ fun CronJobsScreen(
modifier = Modifier.weight(1f),
)
StatusBadge(
text = if (job.state == "active") "Active" else "Paused",
text =
if (job.state == "active") {
stringResource(
R.string.cron_status_active,
)
} else {
stringResource(R.string.cron_status_paused)
},
status =
if (job.state == "active") {
StatusBadgeType.SUCCESS
@@ -143,21 +152,21 @@ fun CronJobsScreen(
onClick = { viewModel.pauseCronJob(job.id) },
modifier = Modifier.weight(1f),
) {
Text("Pause")
Text(stringResource(R.string.cron_action_pause))
}
} else {
OutlinedButton(
onClick = { viewModel.resumeCronJob(job.id) },
modifier = Modifier.weight(1f),
) {
Text("Resume")
Text(stringResource(R.string.cron_action_resume))
}
}
OutlinedButton(
onClick = { viewModel.triggerCronJob(job.id) },
modifier = Modifier.weight(1f),
) {
Text("Run")
Text(stringResource(R.string.cron_action_run))
}
Button(
onClick = { viewModel.deleteCronJob(job.id) },
@@ -167,7 +176,7 @@ fun CronJobsScreen(
),
modifier = Modifier.weight(1f),
) {
Text("Delete")
Text(stringResource(R.string.action_delete))
}
}
}
@@ -32,10 +32,12 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.R
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState
@@ -62,7 +64,7 @@ fun GatewayScreen(
}
HermesScaffold(
title = { Text("Gateway Control") },
title = { Text(stringResource(R.string.screen_gateway)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadStatus() },
@@ -96,7 +98,7 @@ fun GatewayScreen(
)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.loadStatus() }) {
Text("Retry")
Text(stringResource(R.string.action_retry))
}
}
} else {
@@ -132,7 +134,14 @@ fun GatewayScreen(
horizontalAlignment = Alignment.CenterHorizontally,
) {
Text(
text = if (isRunning) "GATEWAY RUNNING" else "GATEWAY STOPPED",
text =
if (isRunning) {
stringResource(
R.string.gateway_status_running,
)
} else {
stringResource(R.string.gateway_status_stopped)
},
style = MaterialTheme.typography.headlineSmall,
fontWeight = FontWeight.Bold,
color =
@@ -145,7 +154,7 @@ fun GatewayScreen(
status?.version?.let {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Version: $it",
text = stringResource(R.string.gateway_label_version, it),
style = MaterialTheme.typography.bodySmall,
color =
if (isRunning) {
@@ -170,7 +179,7 @@ fun GatewayScreen(
verticalArrangement = Arrangement.spacedBy(12.dp),
) {
Text(
text = "Controls",
text = stringResource(R.string.gateway_sec_controls),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
@@ -191,7 +200,7 @@ fun GatewayScreen(
) {
Icon(imageVector = Icons.Filled.PlayArrow, contentDescription = null)
Spacer(modifier = Modifier.width(4.dp))
Text("Start")
Text(stringResource(R.string.gateway_action_start))
}
Button(
@@ -204,7 +213,7 @@ fun GatewayScreen(
contentColor = Color.White,
),
) {
Text("Stop")
Text(stringResource(R.string.gateway_action_stop))
}
}
@@ -219,7 +228,7 @@ fun GatewayScreen(
) {
Icon(imageVector = Icons.Filled.Refresh, contentDescription = null)
Spacer(modifier = Modifier.width(4.dp))
Text("Restart")
Text(stringResource(R.string.gateway_action_restart))
}
if (state.isActionRunning) {
@@ -245,7 +254,7 @@ fun GatewayScreen(
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Text(
text = "Active Platforms",
text = stringResource(R.string.gateway_sec_active_platforms),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.SemiBold,
)
@@ -39,10 +39,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.R
import com.m57.hermescontrol.data.model.KanbanTask
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -73,7 +75,7 @@ fun KanbanScreen(
}
HermesScaffold(
title = { Text("Kanban Board") },
title = { Text(stringResource(R.string.kanban_board_title)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadBoards() },
@@ -99,10 +101,12 @@ fun KanbanScreen(
Text(
text = state.errorMessage ?: "",
color = MaterialTheme.colorScheme.error,
modifier = Modifier.padding(16.dp),
modifier = Modifier.padding(paddingValues),
)
} else {
Column(modifier = Modifier.fillMaxSize()) {
Column(
modifier = Modifier.fillMaxSize().padding(paddingValues),
) {
// Board selector tab row
if (state.boards.isNotEmpty()) {
PrimaryScrollableTabRow(
@@ -121,11 +125,11 @@ fun KanbanScreen(
if (state.selectedBoard == null) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No boards found.")
Text(stringResource(R.string.kanban_no_boards))
}
} else if (state.columns.isEmpty()) {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) {
Text("No columns found for this board.")
Text(stringResource(R.string.kanban_no_columns))
}
} else {
LazyRow(
@@ -219,7 +223,10 @@ fun TaskCard(
) {
if (onMoveLeft != null) {
IconButton(onClick = onMoveLeft) {
Icon(imageVector = Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Move Left")
Icon(
imageVector = Icons.AutoMirrored.Filled.ArrowBack,
contentDescription = stringResource(R.string.content_desc_move_left),
)
}
} else {
Spacer(modifier = Modifier.size(48.dp))
@@ -227,7 +234,10 @@ fun TaskCard(
if (onMoveRight != null) {
IconButton(onClick = onMoveRight) {
Icon(imageVector = Icons.Filled.ArrowForward, contentDescription = "Move Right")
Icon(
imageVector = Icons.Filled.ArrowForward,
contentDescription = stringResource(R.string.content_desc_move_right),
)
}
} else {
Spacer(modifier = Modifier.size(48.dp))
@@ -248,20 +258,20 @@ fun AddTaskDialog(
androidx.compose.material3.AlertDialog(
onDismissRequest = onDismiss,
title = { Text("Add New Task") },
title = { Text(stringResource(R.string.kanban_add_new_task)) },
text = {
Column {
OutlinedTextField(
value = title,
onValueChange = { title = it },
label = { Text("Task Title") },
label = { Text(stringResource(R.string.kanban_task_title)) },
modifier = Modifier.fillMaxWidth(),
)
Spacer(modifier = Modifier.height(8.dp))
OutlinedTextField(
value = desc,
onValueChange = { desc = it },
label = { Text("Task Description") },
label = { Text(stringResource(R.string.kanban_task_desc)) },
modifier = Modifier.fillMaxWidth(),
)
}
@@ -271,12 +281,12 @@ fun AddTaskDialog(
onClick = { if (title.isNotBlank()) onConfirm(title, desc.ifBlank { null }) },
enabled = title.isNotBlank(),
) {
Text("Add")
Text(stringResource(R.string.action_add))
}
},
dismissButton = {
Button(onClick = onDismiss) {
Text("Cancel")
Text(stringResource(R.string.action_cancel))
}
},
)
@@ -36,10 +36,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
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.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -67,7 +69,7 @@ fun KeysScreen(
}
HermesScaffold(
title = { Text("Keys & Credentials") },
title = { Text(stringResource(R.string.screen_keys)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadKeys() },
@@ -87,10 +89,10 @@ fun KeysScreen(
state.envVars.isEmpty() -> {
EmptyState(
title = "No environment keys reported",
subtitle = "Verify configuration or retry",
title = stringResource(R.string.keys_empty_title),
subtitle = stringResource(R.string.toolsets_empty_desc),
onAction = { viewModel.loadKeys() },
actionLabel = "Refresh",
actionLabel = stringResource(R.string.content_desc_refresh),
modifier = Modifier.padding(paddingValues),
)
}
@@ -123,7 +125,7 @@ fun KeysScreen(
} else if (config.isSet) {
config.redactedValue ?: "********"
} else {
"Not configured"
stringResource(R.string.keys_label_not_configured)
}
Card(modifier = Modifier.fillMaxWidth()) {
@@ -158,13 +160,13 @@ fun KeysScreen(
viewModel.updateKey(key, editedValue)
isEditing = false
}) {
Text("Save")
Text(stringResource(R.string.action_save))
}
Spacer(modifier = Modifier.width(8.dp))
Button(onClick = {
isEditing = false
}) {
Text("Cancel")
Text(stringResource(R.string.action_cancel))
}
}
} else {
@@ -196,7 +198,10 @@ fun KeysScreen(
} else {
Icons.Filled.Visibility
},
contentDescription = "Toggle Visibility",
contentDescription =
stringResource(
R.string.keys_action_toggle_visibility,
),
)
}
}
@@ -206,7 +211,10 @@ fun KeysScreen(
}) {
Icon(
imageVector = Icons.Filled.Edit,
contentDescription = "Edit",
contentDescription =
stringResource(
R.string.nav_drawer_section_converse,
),
)
}
}
@@ -18,11 +18,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
@@ -59,7 +61,7 @@ fun LogsScreen(
}
HermesScaffold(
title = { Text("Logs") },
title = { Text(stringResource(R.string.screen_logs)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadLogs() },
@@ -71,7 +73,7 @@ fun LogsScreen(
state.errorMessage != null -> {
ErrorState(
message = state.errorMessage ?: "Unknown error",
message = state.errorMessage ?: stringResource(R.string.error_unknown),
onRetry = { viewModel.loadLogs() },
)
}
@@ -80,8 +82,8 @@ fun LogsScreen(
// B5 (Jun 18 2026, kanban t_2322818d): proper empty-state using
// the shared EmptyState component instead of a bare Text.
EmptyState(
title = "No logs available",
subtitle = "Log output from Hermes will appear here.",
title = stringResource(R.string.logs_empty_title),
subtitle = stringResource(R.string.logs_empty_desc),
icon = Icons.Filled.HistoryEdu,
)
}
@@ -28,11 +28,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
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.R
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState
@@ -59,7 +61,7 @@ fun McpServersScreen(
}
HermesScaffold(
title = { Text("MCP Servers") },
title = { Text(stringResource(R.string.screen_mcp_servers)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadServers() },
@@ -89,7 +91,7 @@ fun McpServersScreen(
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.loadServers() }) {
Text("Retry")
Text(stringResource(R.string.action_retry))
}
}
} else {
@@ -129,7 +131,11 @@ fun McpServersScreen(
fontWeight = FontWeight.Bold,
)
Text(
text = "Transport: ${server.transport ?: "stdio"}",
text =
stringResource(
R.string.mcp_servers_label_transport,
server.transport ?: "stdio",
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -145,7 +151,7 @@ fun McpServersScreen(
Spacer(modifier = Modifier.height(8.dp))
val fullCmd = "${server.command} ${server.args.orEmpty().joinToString(" ")}"
Text(
text = "Command:",
text = stringResource(R.string.mcp_servers_label_command),
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold,
)
@@ -168,13 +174,13 @@ fun McpServersScreen(
onClick = { viewModel.testServer(server.name) },
modifier = Modifier.padding(end = 8.dp),
) {
Text("Test")
Text(stringResource(R.string.mcp_servers_action_test))
}
OutlinedButton(
onClick = { viewModel.deleteServer(server.name) },
) {
Text("Delete")
Text(stringResource(R.string.action_delete))
}
}
}
@@ -35,10 +35,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.R
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -67,7 +69,7 @@ fun ModelScreen(
}
HermesScaffold(
title = { Text("Models") },
title = { Text(stringResource(R.string.screen_models)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadModelOptions() },
@@ -87,10 +89,10 @@ fun ModelScreen(
state.providers.isEmpty() -> {
EmptyState(
title = "No model providers reported",
subtitle = "Verify configuration or retry",
title = stringResource(R.string.model_empty_title),
subtitle = stringResource(R.string.model_empty_desc),
onAction = { viewModel.loadModelOptions() },
actionLabel = "Refresh",
actionLabel = stringResource(R.string.content_desc_refresh),
modifier = Modifier.padding(paddingValues),
)
}
@@ -107,7 +109,10 @@ fun ModelScreen(
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp))
IconButton(onClick = { viewModel.loadModelOptions() }) {
Icon(imageVector = Icons.Filled.Refresh, contentDescription = "Retry")
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = stringResource(R.string.action_retry),
)
}
}
} else {
@@ -153,7 +158,7 @@ fun ModelScreen(
fontWeight = FontWeight.Bold,
)
Text(
text = "Slug: ${provider.slug}",
text = stringResource(R.string.model_label_slug, provider.slug),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -161,7 +166,7 @@ fun ModelScreen(
if (isCurrent) {
Text(
text = "CURRENT",
text = stringResource(R.string.model_status_current),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold,
@@ -183,7 +188,7 @@ fun ModelScreen(
AnimatedVisibility(visible = isExpanded) {
Column(modifier = Modifier.padding(top = 16.dp)) {
Text(
text = "Available Models:",
text = stringResource(R.string.model_label_available),
style = MaterialTheme.typography.titleSmall,
fontWeight = FontWeight.SemiBold,
modifier = Modifier.padding(bottom = 8.dp),
@@ -192,7 +197,7 @@ fun ModelScreen(
val models = provider.models.orEmpty()
if (models.isEmpty()) {
Text(
text = "No models reported or provider needs authentication.",
text = stringResource(R.string.model_no_models),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -262,7 +267,10 @@ fun ModelScreen(
if (isActive) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = "Active Model",
contentDescription =
stringResource(
R.string.content_desc_active_model,
),
tint = MaterialTheme.colorScheme.primary,
)
}
@@ -28,10 +28,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.R
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState
@@ -58,7 +60,7 @@ fun PairingScreen(
}
HermesScaffold(
title = { Text("Device Pairing") },
title = { Text(stringResource(R.string.pairing_screen_title)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadPairing() },
@@ -88,7 +90,7 @@ fun PairingScreen(
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.loadPairing() }) {
Text("Retry")
Text(stringResource(R.string.action_retry))
}
}
} else {
@@ -109,7 +111,7 @@ fun PairingScreen(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "Pending Approvals",
text = stringResource(R.string.pairing_sec_pending),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
@@ -121,7 +123,7 @@ fun PairingScreen(
contentColor = MaterialTheme.colorScheme.error,
),
) {
Text("Clear All")
Text(stringResource(R.string.pairing_action_clear_all))
}
}
}
@@ -170,7 +172,7 @@ fun PairingScreen(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text("Approve")
Text(stringResource(R.string.pairing_action_approve))
}
}
}
@@ -179,7 +181,7 @@ fun PairingScreen(
item {
Text(
text = "Approved Devices",
text = stringResource(R.string.pairing_sec_approved),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
@@ -198,7 +200,7 @@ fun PairingScreen(
contentAlignment = Alignment.Center,
) {
Text(
text = "No approved devices paired yet.",
text = stringResource(R.string.pairing_empty_approved),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -225,7 +227,11 @@ fun PairingScreen(
fontWeight = FontWeight.SemiBold,
)
Text(
text = "User ID: ${item.user_id ?: "Unknown"}",
text =
stringResource(
R.string.pairing_label_user_id,
item.user_id ?: "Unknown",
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -246,7 +252,7 @@ fun PairingScreen(
contentColor = MaterialTheme.colorScheme.error,
),
) {
Text("Revoke")
Text(stringResource(R.string.pairing_action_revoke))
}
}
}
@@ -28,9 +28,11 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -58,7 +60,7 @@ fun PluginsScreen(
}
HermesScaffold(
title = { Text("Plugins") },
title = { Text(stringResource(R.string.screen_plugins)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadPlugins() },
@@ -78,10 +80,10 @@ fun PluginsScreen(
state.plugins.isEmpty() -> {
EmptyState(
title = "No plugins found",
subtitle = "Verify configuration or retry",
title = stringResource(R.string.plugins_empty_title),
subtitle = stringResource(R.string.plugins_empty_desc),
onAction = { viewModel.loadPlugins() },
actionLabel = "Refresh",
actionLabel = stringResource(R.string.content_desc_refresh),
modifier = Modifier.padding(paddingValues),
)
}
@@ -129,7 +131,7 @@ fun PluginsScreen(
}
Spacer(modifier = Modifier.height(4.dp))
Text(
text = plugin.description ?: "No description provided.",
text = plugin.description ?: stringResource(R.string.plugins_no_desc),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(modifier = Modifier.height(8.dp))
@@ -139,7 +141,7 @@ fun PluginsScreen(
) {
if (plugin.installed) {
OutlinedButton(onClick = { viewModel.updatePlugin(plugin.name) }) {
Text("Update")
Text(stringResource(R.string.plugins_action_update))
}
Spacer(modifier = Modifier.width(8.dp))
OutlinedButton(
@@ -149,11 +151,11 @@ fun PluginsScreen(
contentColor = MaterialTheme.colorScheme.error,
),
) {
Text("Uninstall")
Text(stringResource(R.string.plugins_action_uninstall))
}
} else {
Button(onClick = { viewModel.installPlugin(plugin.name) }) {
Text("Install")
Text(stringResource(R.string.plugins_action_install))
}
}
}
@@ -35,10 +35,12 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.R
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -71,7 +73,7 @@ fun ProfilesScreen(
}
HermesScaffold(
title = { Text("Profiles") },
title = { Text(stringResource(R.string.screen_profiles)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadProfiles() },
@@ -91,10 +93,10 @@ fun ProfilesScreen(
state.profiles.isEmpty() -> {
EmptyState(
title = "No profiles available",
subtitle = "Verify configuration or retry",
title = stringResource(R.string.profiles_empty_title),
subtitle = stringResource(R.string.toolsets_empty_desc),
onAction = { viewModel.loadProfiles() },
actionLabel = "Refresh",
actionLabel = stringResource(R.string.content_desc_refresh),
modifier = Modifier.padding(paddingValues),
)
}
@@ -111,7 +113,7 @@ fun ProfilesScreen(
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.loadProfiles() }) {
Text("Retry")
Text(stringResource(R.string.action_retry))
}
}
} else {
@@ -170,7 +172,10 @@ fun ProfilesScreen(
if (isActive) {
Icon(
imageVector = Icons.Filled.Check,
contentDescription = "Active Profile",
contentDescription =
stringResource(
R.string.profiles_content_desc_active,
),
tint = MaterialTheme.colorScheme.primary,
modifier = Modifier.padding(start = 8.dp),
)
@@ -186,11 +191,19 @@ fun ProfilesScreen(
) {
Column {
Text(
text = "Model: ${profile.model ?: "None"}",
text =
stringResource(
R.string.profiles_label_model,
profile.model ?: "None",
),
style = MaterialTheme.typography.bodyMedium,
)
Text(
text = "Provider: ${profile.provider ?: "None"}",
text =
stringResource(
R.string.profiles_label_provider,
profile.provider ?: "None",
),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -216,7 +229,7 @@ fun ProfilesScreen(
modifier = Modifier.width(16.dp),
)
Spacer(modifier = Modifier.width(4.dp))
Text("Edit Soul")
Text(stringResource(R.string.profiles_action_edit_soul))
}
Button(
@@ -226,7 +239,7 @@ fun ProfilesScreen(
tempModelName = profile.model ?: ""
},
) {
Text("Set Model")
Text(stringResource(R.string.profiles_action_set_model))
}
}
}
@@ -21,11 +21,13 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
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.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.ChatScreen
import com.m57.hermescontrol.NavigationController
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
@@ -48,7 +50,7 @@ fun SessionsScreen(
}
HermesScaffold(
title = { Text("History") },
title = { Text(stringResource(R.string.screen_history)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadSessions() },
@@ -61,15 +63,15 @@ fun SessionsScreen(
state.errorMessage != null -> {
ErrorState(
message = state.errorMessage ?: "Unknown error",
message = state.errorMessage ?: stringResource(R.string.error_unknown),
onRetry = { viewModel.loadSessions() },
)
}
state.sessions.isEmpty() -> {
EmptyState(
title = "No history",
subtitle = "Previous chat sessions from Hermes will appear here.",
title = stringResource(R.string.history_empty_title),
subtitle = stringResource(R.string.history_empty_desc),
icon = Icons.Filled.History,
)
}
@@ -98,7 +100,7 @@ fun SessionsScreen(
modifier = Modifier.padding(spacing.md),
) {
Text(
text = session.title ?: "Untitled",
text = session.title ?: stringResource(R.string.history_untitled),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
@@ -107,7 +109,11 @@ fun SessionsScreen(
verticalAlignment = Alignment.CenterVertically,
) {
Text(
text = "${session.message_count ?: 0} messages",
text =
stringResource(
R.string.history_message_count,
session.message_count ?: 0,
),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -53,6 +53,7 @@ import androidx.compose.runtime.remember
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.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
@@ -61,6 +62,7 @@ import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.BuildConfig
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.ThemePreference
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -77,7 +79,7 @@ fun SettingsScreen(
BackHandler(onBack = onBack)
HermesScaffold(
title = { Text("Settings") },
title = { Text(stringResource(R.string.screen_settings)) },
showBack = true,
onBack = onBack,
) {
@@ -90,12 +92,12 @@ fun SettingsScreen(
verticalArrangement = Arrangement.spacedBy(16.dp),
) {
// Connection section
SectionCard(title = "Connection") {
SectionCard(title = stringResource(R.string.settings_sec_connection)) {
// Connection Profiles management in Settings
if (state.profiles.isNotEmpty()) {
var profilesExpanded by remember { mutableStateOf(false) }
Text(
text = "Connection Profile",
text = stringResource(R.string.settings_item_profile),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -105,7 +107,7 @@ fun SettingsScreen(
modifier = Modifier.fillMaxWidth(),
) {
val activeProfile = state.profiles.firstOrNull { it.id == state.selectedProfileId }
Text(text = activeProfile?.name ?: "Standalone / Default")
Text(text = activeProfile?.name ?: stringResource(R.string.settings_profile_default))
}
DropdownMenu(
expanded = profilesExpanded,
@@ -113,7 +115,7 @@ fun SettingsScreen(
modifier = Modifier.fillMaxWidth(0.85f),
) {
DropdownMenuItem(
text = { Text("None (Standalone / Default)") },
text = { Text(stringResource(R.string.settings_profile_none)) },
onClick = {
viewModel.selectProfile(null)
profilesExpanded = false
@@ -136,7 +138,10 @@ fun SettingsScreen(
) {
Icon(
imageVector = Icons.Filled.Close,
contentDescription = "Delete Profile",
contentDescription =
stringResource(
R.string.content_desc_delete_profile,
),
tint = MaterialTheme.colorScheme.error,
)
}
@@ -155,7 +160,7 @@ fun SettingsScreen(
OutlinedTextField(
value = state.renameProfileName,
onValueChange = viewModel::onRenameProfileNameChange,
label = { Text("Rename Profile") },
label = { Text(stringResource(R.string.settings_action_rename_profile)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
trailingIcon = {
@@ -163,7 +168,7 @@ fun SettingsScreen(
onClick = viewModel::renameProfile,
modifier = Modifier.padding(end = 4.dp),
) {
Text("Rename")
Text(stringResource(R.string.action_rename))
}
},
)
@@ -174,7 +179,7 @@ fun SettingsScreen(
OutlinedTextField(
value = state.host,
onValueChange = viewModel::onHostChange,
label = { Text("Host") },
label = { Text(stringResource(R.string.settings_field_host)) },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
colors =
@@ -189,7 +194,7 @@ fun SettingsScreen(
OutlinedTextField(
value = state.port,
onValueChange = viewModel::onPortChange,
label = { Text("Port") },
label = { Text(stringResource(R.string.settings_field_port)) },
singleLine = true,
keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number),
modifier = Modifier.fillMaxWidth(),
@@ -205,7 +210,7 @@ fun SettingsScreen(
OutlinedTextField(
value = state.token,
onValueChange = viewModel::onTokenChange,
label = { Text("Auth Token") },
label = { Text(stringResource(R.string.settings_field_token)) },
singleLine = true,
visualTransformation =
if (passwordVisible) {
@@ -224,9 +229,9 @@ fun SettingsScreen(
},
contentDescription =
if (passwordVisible) {
"Hide token"
stringResource(R.string.content_desc_hide_token)
} else {
"Show token"
stringResource(R.string.content_desc_show_token)
},
)
}
@@ -253,12 +258,12 @@ fun SettingsScreen(
},
modifier = Modifier.fillMaxWidth(),
) {
Text("Clear Token")
Text(stringResource(R.string.settings_action_clear_token))
}
}
// Behavior section
SectionCard(title = "Behavior") {
SectionCard(title = stringResource(R.string.settings_sec_behavior)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -266,11 +271,11 @@ fun SettingsScreen(
) {
Column {
Text(
text = "Auto-Reconnect",
text = stringResource(R.string.settings_item_auto_reconnect),
style = MaterialTheme.typography.bodyLarge,
)
Text(
text = "Automatically reconnect on disconnect",
text = stringResource(R.string.settings_desc_auto_reconnect),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -290,9 +295,9 @@ fun SettingsScreen(
}
// Appearance section
SectionCard(title = "Appearance") {
SectionCard(title = stringResource(R.string.settings_sec_appearance)) {
Text(
text = "Theme",
text = stringResource(R.string.settings_item_theme),
style = MaterialTheme.typography.bodyLarge,
)
Spacer(modifier = Modifier.height(8.dp))
@@ -308,9 +313,11 @@ fun SettingsScreen(
),
) {
Text(
pref.name
.lowercase()
.replaceFirstChar { it.uppercase() },
when (pref) {
ThemePreference.SYSTEM -> stringResource(R.string.theme_system)
ThemePreference.LIGHT -> stringResource(R.string.theme_light)
ThemePreference.DARK -> stringResource(R.string.theme_dark)
},
)
}
}
@@ -318,7 +325,7 @@ fun SettingsScreen(
}
// Chat section
SectionCard(title = "Chat") {
SectionCard(title = stringResource(R.string.settings_sec_chat)) {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -326,11 +333,11 @@ fun SettingsScreen(
) {
Column {
Text(
text = "Typing Effect",
text = stringResource(R.string.settings_item_typing_effect),
style = MaterialTheme.typography.bodyLarge,
)
Text(
text = "Animate new messages word by word",
text = stringResource(R.string.settings_desc_typing_effect),
style =
MaterialTheme.typography.bodySmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -352,7 +359,7 @@ fun SettingsScreen(
AnimatedVisibility(visible = state.typingEffectEnabled) {
Column(modifier = Modifier.padding(top = 12.dp)) {
Text(
text = "Delay: ${state.typingEffectDelayMs} ms",
text = stringResource(R.string.settings_item_typing_delay, state.typingEffectDelayMs),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(modifier = Modifier.height(4.dp))
@@ -368,14 +375,14 @@ fun SettingsScreen(
horizontalArrangement = Arrangement.SpaceBetween,
) {
Text(
text = "10 ms",
text = stringResource(R.string.settings_delay_10ms),
style =
MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
),
)
Text(
text = "100 ms",
text = stringResource(R.string.settings_delay_100ms),
style =
MaterialTheme.typography.labelSmall.copy(
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -387,15 +394,16 @@ fun SettingsScreen(
}
// Navigation Bar section
SectionCard(title = "Navigation Bar") {
SectionCard(title = stringResource(R.string.settings_sec_nav_bar)) {
Text(
text = "Bottom bar items (${state.selectedNavItems.size}/5)",
text = stringResource(R.string.settings_item_nav_items, state.selectedNavItems.size),
style = MaterialTheme.typography.bodyMedium,
)
Spacer(modifier = Modifier.height(8.dp))
state.selectedNavItems.forEachIndexed { index, name ->
val label = viewModel.availableNavItems.firstOrNull { it.first == name }?.second ?: name
val labelRes = viewModel.availableNavItems.firstOrNull { it.first == name }?.second
val label = if (labelRes != null) stringResource(labelRes) else name
Row(
modifier = Modifier.fillMaxWidth().padding(vertical = 2.dp),
horizontalArrangement = Arrangement.SpaceBetween,
@@ -417,7 +425,10 @@ fun SettingsScreen(
},
enabled = index > 0,
) {
Icon(Icons.Filled.KeyboardArrowUp, contentDescription = "Move up")
Icon(
Icons.Filled.KeyboardArrowUp,
contentDescription = stringResource(R.string.content_desc_move_up),
)
}
IconButton(
onClick = {
@@ -429,10 +440,16 @@ fun SettingsScreen(
},
enabled = index < state.selectedNavItems.lastIndex,
) {
Icon(Icons.Filled.KeyboardArrowDown, contentDescription = "Move down")
Icon(
Icons.Filled.KeyboardArrowDown,
contentDescription = stringResource(R.string.content_desc_move_down),
)
}
IconButton(onClick = { viewModel.removeNavItem(name) }) {
Icon(Icons.Filled.Close, contentDescription = "Remove $label")
Icon(
Icons.Filled.Close,
contentDescription = stringResource(R.string.content_desc_remove_item, label),
)
}
}
}
@@ -446,12 +463,12 @@ fun SettingsScreen(
OutlinedButton(onClick = { expanded = true }) {
Icon(Icons.Filled.Add, contentDescription = null)
Spacer(Modifier.size(4.dp))
Text("Add Item")
Text(stringResource(R.string.settings_action_add_item))
}
DropdownMenu(expanded = expanded, onDismissRequest = { expanded = false }) {
available.forEach { (name, label) ->
available.forEach { (name, labelRes) ->
DropdownMenuItem(
text = { Text(label) },
text = { Text(stringResource(labelRes)) },
onClick = {
viewModel.addNavItem(name)
expanded = false
@@ -463,7 +480,7 @@ fun SettingsScreen(
}
} else {
Text(
text = "Maximum of 5 items",
text = stringResource(R.string.settings_nav_max_reached),
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -505,7 +522,7 @@ fun SettingsScreen(
exit = fadeOut(),
) {
Text(
text = "✅ Settings saved",
text = stringResource(R.string.settings_save_success),
style =
MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.primary,
@@ -529,7 +546,7 @@ fun SettingsScreen(
strokeWidth = 2.dp,
)
} else {
Text("Test Connection")
Text(stringResource(R.string.settings_action_test_connection))
}
}
@@ -541,17 +558,17 @@ fun SettingsScreen(
containerColor = MaterialTheme.colorScheme.primary,
),
) {
Text("Save")
Text(stringResource(R.string.action_save))
}
}
Spacer(modifier = Modifier.height(16.dp))
// ── About section ─────────────────────────────────────────
SectionCard(title = "About") {
SectionCard(title = stringResource(R.string.settings_sec_about)) {
Row(verticalAlignment = Alignment.CenterVertically) {
Text(
text = "⚡ Hermes Control",
text = stringResource(R.string.settings_about_app_name),
style =
MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
@@ -563,13 +580,21 @@ fun SettingsScreen(
HorizontalDivider(color = MaterialTheme.colorScheme.outlineVariant)
Spacer(modifier = Modifier.height(8.dp))
InfoRow(label = "Version", value = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})")
InfoRow(
label = "Build",
value = if (BuildConfig.DEBUG) "Debug" else "Release",
label = stringResource(R.string.settings_about_version),
value = "v${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})",
)
InfoRow(
label = stringResource(R.string.settings_about_build),
value =
if (BuildConfig.DEBUG) {
stringResource(R.string.settings_about_debug)
} else {
stringResource(R.string.settings_about_release)
},
)
if (BuildConfig.GIT_SHA.isNotBlank()) {
InfoRow(label = "Commit", value = BuildConfig.GIT_SHA)
InfoRow(label = stringResource(R.string.settings_about_commit), value = BuildConfig.GIT_SHA)
}
Spacer(modifier = Modifier.height(12.dp))
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.settings
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.R
import com.m57.hermescontrol.data.local.AuthManager
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.NetworkResult
@@ -58,10 +59,11 @@ class SettingsViewModel : ViewModel() {
profiles = AuthManager.getConnectionProfiles(),
selectedProfileId = selectedId,
renameProfileName =
AuthManager.getConnectionProfiles().firstOrNull {
p ->
p.id == selectedId
}?.name ?: "",
AuthManager
.getConnectionProfiles()
.firstOrNull { p ->
p.id == selectedId
}?.name ?: "",
)
}
}
@@ -128,28 +130,28 @@ class SettingsViewModel : ViewModel() {
}
/** All screens available for bottom-nav selection (name → display label). */
val availableNavItems: List<Pair<String, String>> =
val availableNavItems: List<Pair<String, Int>> =
listOf(
"ChatScreen" to "Chat",
"SkillsScreen" to "Skills",
"CronJobsScreen" to "Cron Jobs",
"SystemScreen" to "System",
"SettingsScreen" to "Settings",
"ProfilesScreen" to "Profiles",
"WebhooksScreen" to "Webhooks",
"GatewayScreen" to "Gateway",
"HistoryScreen" to "History",
"ToolsetsScreen" to "Toolsets",
"PluginsScreen" to "Plugins",
"ConfigScreen" to "Config",
"McpServersScreen" to "MCP Servers",
"ModelScreen" to "Models",
"PairingScreen" to "Pairing",
"KeysScreen" to "Keys",
"ChannelsScreen" to "Channels",
"LogsScreen" to "Logs",
"KanbanScreen" to "Kanban",
"AchievementsScreen" to "Achievements",
"ChatScreen" to R.string.screen_chat,
"SkillsScreen" to R.string.screen_skills,
"CronJobsScreen" to R.string.screen_cron,
"SystemScreen" to R.string.screen_system,
"SettingsScreen" to R.string.screen_settings,
"ProfilesScreen" to R.string.screen_profiles,
"WebhooksScreen" to R.string.screen_webhooks,
"GatewayScreen" to R.string.screen_gateway,
"HistoryScreen" to R.string.screen_history,
"ToolsetsScreen" to R.string.screen_toolsets,
"PluginsScreen" to R.string.screen_plugins,
"ConfigScreen" to R.string.screen_config,
"McpServersScreen" to R.string.screen_mcp_servers,
"ModelScreen" to R.string.screen_models,
"PairingScreen" to R.string.screen_pairing,
"KeysScreen" to R.string.screen_keys,
"ChannelsScreen" to R.string.screen_channels,
"LogsScreen" to R.string.screen_logs,
"KanbanScreen" to R.string.screen_kanban,
"AchievementsScreen" to R.string.screen_achievements,
)
/** Add a nav item to the bottom bar (max 5). Auto-saves. */
@@ -35,6 +35,7 @@ import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontWeight
@@ -43,6 +44,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
@@ -67,7 +69,11 @@ fun SkillsScreen(
var selectedStatus by remember { mutableStateOf("All Statuses") }
val categories =
remember(state.skills) {
listOf("All") + state.skills.mapNotNull { it.category }.distinct().sorted()
listOf("All") +
state.skills
.mapNotNull { it.category }
.distinct()
.sorted()
}
var selectedCategory by remember { mutableStateOf("All") }
@@ -113,7 +119,7 @@ fun SkillsScreen(
}
HermesScaffold(
title = { Text("Skills") },
title = { Text(stringResource(R.string.screen_skills)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadSkills() },
@@ -125,15 +131,15 @@ fun SkillsScreen(
state.errorMessage != null -> {
ErrorState(
message = state.errorMessage ?: "Unknown error",
message = state.errorMessage ?: stringResource(R.string.error_unknown),
onRetry = { viewModel.loadSkills() },
)
}
state.skills.isEmpty() -> {
EmptyState(
title = "No skills found",
subtitle = "Skills loaded from Hermes will appear here.",
title = stringResource(R.string.skills_no_skills),
subtitle = stringResource(R.string.skills_no_skills_desc),
icon = Icons.Filled.Extension,
)
}
@@ -148,7 +154,7 @@ fun SkillsScreen(
SearchBar(
query = query,
onQueryChange = { query = it },
placeholder = "Search skills…",
placeholder = stringResource(R.string.skills_search_placeholder),
)
}
item {
@@ -160,7 +166,7 @@ fun SkillsScreen(
verticalArrangement = Arrangement.spacedBy(spacing.xs),
) {
Text(
text = "Status",
text = stringResource(R.string.skills_filter_status),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -171,7 +177,16 @@ fun SkillsScreen(
FilterChip(
selected = selectedStatus == status,
onClick = { selectedStatus = status },
label = { Text(status) },
label = {
Text(
when (status) {
"All Statuses" -> stringResource(R.string.skills_status_all)
"Enabled" -> stringResource(R.string.skills_status_enabled)
"Disabled" -> stringResource(R.string.skills_status_disabled)
else -> status
},
)
},
)
}
}
@@ -187,7 +202,7 @@ fun SkillsScreen(
verticalArrangement = Arrangement.spacedBy(spacing.xs),
) {
Text(
text = "Category",
text = stringResource(R.string.skills_filter_category),
style = MaterialTheme.typography.labelMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
modifier = Modifier.padding(horizontal = spacing.md),
@@ -214,14 +229,14 @@ fun SkillsScreen(
verticalArrangement = Arrangement.spacedBy(spacing.xs),
) {
Text(
text = "No matching skills",
text = stringResource(R.string.skills_no_matching),
style =
MaterialTheme.typography.titleMedium.copy(
fontWeight = FontWeight.Bold,
),
)
Text(
text = "Try adjusting your search or filters.",
text = stringResource(R.string.skills_no_matching_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -289,7 +304,7 @@ fun SkillsScreen(
) {
Icon(
imageVector = Icons.Filled.Edit,
contentDescription = "Edit Skill File",
contentDescription = stringResource(R.string.content_desc_edit_skill),
tint = MaterialTheme.colorScheme.onSurfaceVariant,
)
}
@@ -369,7 +384,7 @@ fun SkillEditorDialog(
),
) {
HermesScaffold(
title = { Text("Edit: $skillName") },
title = { Text(stringResource(R.string.skills_edit_title, skillName)) },
onBack = {
if (hasChanges) {
showDiscardConfirm = true
@@ -393,7 +408,7 @@ fun SkillEditorDialog(
} else {
Icon(
imageVector = Icons.Filled.Save,
contentDescription = "Save Changes",
contentDescription = stringResource(R.string.content_desc_save_changes),
)
}
}
@@ -425,7 +440,7 @@ fun SkillEditorDialog(
color = MaterialTheme.colorScheme.onSurface,
),
placeholder = {
Text("Enter skill content...")
Text(stringResource(R.string.skills_content_placeholder))
},
)
}
@@ -438,8 +453,8 @@ fun SkillEditorDialog(
if (showDiscardConfirm) {
AlertDialog(
onDismissRequest = { showDiscardConfirm = false },
title = { Text("Discard changes?") },
text = { Text("You have unsaved changes. Are you sure you want to discard them?") },
title = { Text(stringResource(R.string.skills_discard_title)) },
text = { Text(stringResource(R.string.skills_discard_message)) },
confirmButton = {
TextButton(
onClick = {
@@ -447,12 +462,12 @@ fun SkillEditorDialog(
onDismiss()
},
) {
Text("Discard")
Text(stringResource(R.string.action_discard))
}
},
dismissButton = {
TextButton(onClick = { showDiscardConfirm = false }) {
Text("Cancel")
Text(stringResource(R.string.action_cancel))
}
},
)
@@ -27,8 +27,10 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.viewmodel.compose.viewModel
import com.m57.hermescontrol.R
import com.m57.hermescontrol.theme.LocalHermesStatusColors
import com.m57.hermescontrol.theme.LocalSpacing
import com.m57.hermescontrol.ui.common.EmptyState
@@ -65,7 +67,7 @@ fun SystemScreen(
}
HermesScaffold(
title = { Text("System") },
title = { Text(stringResource(R.string.screen_system)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadSystemData() },
@@ -77,15 +79,15 @@ fun SystemScreen(
state.errorMessage != null -> {
ErrorState(
message = state.errorMessage ?: "Unknown error",
message = state.errorMessage ?: stringResource(R.string.error_unknown),
onRetry = { viewModel.loadSystemData() },
)
}
state.stats == null && state.doctorReport == null -> {
EmptyState(
title = "No system data",
subtitle = "Tap refresh to load system diagnostics.",
title = stringResource(R.string.system_empty_title),
subtitle = stringResource(R.string.system_empty_desc),
)
}
@@ -102,7 +104,7 @@ fun SystemScreen(
// Stats row
state.stats?.let { stats ->
item {
SectionHeader(title = "Performance")
SectionHeader(title = stringResource(R.string.system_sec_performance))
}
item {
FlowRow(
@@ -111,7 +113,7 @@ fun SystemScreen(
) {
stats.cpuPercent?.let { cpu ->
StatCard(
label = "CPU Usage",
label = stringResource(R.string.system_metric_cpu),
value = "$cpu%",
icon = Icons.Filled.Speed,
modifier = Modifier.weight(1f),
@@ -119,7 +121,7 @@ fun SystemScreen(
}
stats.memoryPercent?.let { mem ->
StatCard(
label = "Memory",
label = stringResource(R.string.system_metric_memory),
value = "$mem%",
icon = Icons.Filled.Memory,
modifier = Modifier.weight(1f),
@@ -132,7 +134,7 @@ fun SystemScreen(
// Doctor diagnostics
state.doctorReport?.let { report ->
item {
SectionHeader(title = "Doctor")
SectionHeader(title = stringResource(R.string.system_sec_doctor))
}
item {
Card(
@@ -158,7 +160,12 @@ fun SystemScreen(
},
)
StatusBadge(
text = if (report.ok) "Running" else "Failed",
text =
if (report.ok) {
stringResource(R.string.system_status_running)
} else {
stringResource(R.string.system_status_failed)
},
status =
if (report.ok) {
StatusBadgeType.SUCCESS
@@ -169,10 +176,13 @@ fun SystemScreen(
}
Spacer(modifier = Modifier.height(spacing.sm))
report.pid?.let { pid ->
InfoRow(label = "Process PID", value = pid.toString())
InfoRow(
label = stringResource(R.string.system_info_pid),
value = pid.toString(),
)
}
report.name?.let { name ->
InfoRow(label = "Process Name", value = name)
InfoRow(label = stringResource(R.string.system_info_proc_name), value = name)
}
}
}
@@ -181,7 +191,7 @@ fun SystemScreen(
// Admin actions
item {
SectionHeader(title = "Administration")
SectionHeader(title = stringResource(R.string.system_sec_admin))
}
item {
Card(
@@ -201,7 +211,7 @@ fun SystemScreen(
contentDescription = null,
modifier = Modifier.padding(end = spacing.xs),
)
Text("Trigger System Backup")
Text(stringResource(R.string.system_action_backup))
}
}
}
@@ -32,10 +32,12 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
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.R
import com.m57.hermescontrol.ui.common.EmptyState
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
@@ -63,7 +65,7 @@ fun ToolsetsScreen(
}
HermesScaffold(
title = { Text("Toolsets") },
title = { Text(stringResource(R.string.screen_toolsets)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadToolsets() },
@@ -83,10 +85,10 @@ fun ToolsetsScreen(
state.toolsets.isEmpty() -> {
EmptyState(
title = "No toolsets reported",
subtitle = "Verify configuration or retry",
title = stringResource(R.string.toolsets_empty_title),
subtitle = stringResource(R.string.toolsets_empty_desc),
onAction = { viewModel.loadToolsets() },
actionLabel = "Refresh",
actionLabel = stringResource(R.string.content_desc_refresh),
modifier = Modifier.padding(paddingValues),
)
}
@@ -103,7 +105,10 @@ fun ToolsetsScreen(
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp))
IconButton(onClick = { viewModel.loadToolsets() }) {
Icon(imageVector = Icons.Filled.Refresh, contentDescription = "Retry")
Icon(
imageVector = Icons.Filled.Refresh,
contentDescription = stringResource(R.string.action_retry),
)
}
}
} else {
@@ -29,11 +29,13 @@ import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
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.R
import com.m57.hermescontrol.ui.common.ErrorState
import com.m57.hermescontrol.ui.common.HermesScaffold
import com.m57.hermescontrol.ui.common.LoadingState
@@ -60,7 +62,7 @@ fun WebhooksScreen(
}
HermesScaffold(
title = { Text("Webhooks") },
title = { Text(stringResource(R.string.screen_webhooks)) },
onOpenDrawer = onOpenDrawer,
isRefreshing = state.isLoading,
onRefresh = { viewModel.loadWebhooks() },
@@ -90,7 +92,7 @@ fun WebhooksScreen(
Text(text = state.errorMessage ?: "", color = MaterialTheme.colorScheme.error)
Spacer(modifier = Modifier.height(16.dp))
Button(onClick = { viewModel.loadWebhooks() }) {
Text("Retry")
Text(stringResource(R.string.action_retry))
}
}
} else {
@@ -123,7 +125,7 @@ fun WebhooksScreen(
) {
Column(modifier = Modifier.weight(1f)) {
Text(
text = "Global Webhooks Status",
text = stringResource(R.string.webhooks_sec_status),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
@@ -147,7 +149,7 @@ fun WebhooksScreen(
// Subscriptions List Header
item {
Text(
text = "Subscriptions",
text = stringResource(R.string.webhooks_sec_subscriptions),
style = MaterialTheme.typography.titleMedium,
fontWeight = FontWeight.Bold,
)
@@ -166,7 +168,7 @@ fun WebhooksScreen(
contentAlignment = Alignment.Center,
) {
Text(
text = "No webhook subscriptions configured.",
text = stringResource(R.string.webhooks_empty_desc),
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant,
)
@@ -191,7 +193,7 @@ fun WebhooksScreen(
fontWeight = FontWeight.SemiBold,
)
Text(
text = "Target URL:",
text = stringResource(R.string.webhooks_label_target),
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold,
)
@@ -207,7 +209,7 @@ fun WebhooksScreen(
if (events.isNotEmpty()) {
Spacer(modifier = Modifier.height(4.dp))
Text(
text = "Events Subscribed:",
text = stringResource(R.string.webhooks_label_events),
style = MaterialTheme.typography.bodySmall,
fontWeight = FontWeight.Bold,
)
+305
View File
@@ -13,4 +13,309 @@
<string name="chat_prev_match_desc">Previous match</string>
<string name="chat_next_match_desc">Next match</string>
<string name="chat_close_search_desc">Close search</string>
<!-- Actions -->
<string name="action_retry">Retry</string>
<string name="action_cancel">Cancel</string>
<string name="action_ok">OK</string>
<string name="action_delete">Delete</string>
<!-- Content Descriptions -->
<string name="content_desc_back">Back</string>
<string name="content_desc_open_drawer">Open navigation drawer</string>
<string name="content_desc_refresh">Refresh</string>
<string name="content_desc_clear_search">Clear search</string>
<!-- General Errors -->
<string name="error_unknown">Unknown error</string>
<!-- Navigation & Drawer -->
<string name="nav_drawer_title">⚡ Hermes</string>
<string name="nav_drawer_subtitle">AI Agent Control</string>
<string name="nav_drawer_section_converse">Converse</string>
<string name="nav_drawer_section_automate">Automate</string>
<string name="nav_drawer_section_configure">Configure</string>
<string name="nav_drawer_section_inspect">Inspect</string>
<!-- Screen Names -->
<string name="screen_chat">Chat</string>
<string name="screen_skills">Skills</string>
<string name="screen_cron">Cron</string>
<string name="screen_system">System</string>
<string name="screen_settings">Settings</string>
<string name="screen_profiles">Profiles</string>
<string name="screen_webhooks">Webhooks</string>
<string name="screen_gateway">Gateway</string>
<string name="screen_toolsets">Toolsets</string>
<string name="screen_plugins">Plugins</string>
<string name="screen_config">Config</string>
<string name="screen_mcp_servers">MCP Servers</string>
<string name="screen_models">Models</string>
<string name="screen_pairing">Pairing</string>
<string name="screen_keys">Keys</string>
<string name="screen_channels">Channels</string>
<string name="screen_logs">Logs</string>
<string name="screen_kanban">Kanban</string>
<string name="screen_achievements">Achievements</string>
<string name="screen_history">History</string>
<!-- Notifications -->
<string name="notif_title">Hermes</string>
<string name="notif_waiting_replies">Waiting for Hermes replies</string>
<string name="notif_new_message">New message</string>
<string name="notif_clarification_needed">Hermes needs a clarification</string>
<string name="notif_reply_placeholder">Type your reply\u2026</string>
<string name="notif_channel_service_name">Hermes Background Service</string>
<string name="notif_channel_service_desc">Keeps connection alive in the background</string>
<string name="notif_channel_chat_name">Hermes Chat Replies</string>
<string name="notif_channel_chat_desc">Alerts for new replies from Hermes</string>
<string name="action_reply">Reply</string>
<!-- Chat Screen & Bubble -->
<string name="chat_action_search">Search</string>
<string name="chat_action_close_search">Close search</string>
<string name="chat_action_reconnect">Reconnect</string>
<string name="chat_empty_title">Ready to chat</string>
<string name="chat_empty_subtitle">Send a message to start a conversation</string>
<string name="chat_status_connecting">Connecting to Hermes\u2026</string>
<string name="chat_status_reconnecting">Reconnecting to Hermes\u2026</string>
<string name="chat_status_disconnected">Disconnected from Hermes</string>
<string name="chat_thinking">Thinking\u2026</string>
<string name="chat_thinking_param">Thinking: %1$s\u2026</string>
<string name="chat_input_placeholder_not_connected">Not connected\u2026</string>
<string name="chat_input_placeholder_waiting">Waiting for response\u2026</string>
<string name="chat_input_placeholder_type_message">Type a message\u2026</string>
<string name="chat_tool_stderr">Error Output (stderr):\n%1$s</string>
<string name="chat_tool_execution_error">Execution Error: %1$s</string>
<string name="chat_tool_exit_code">Exit Code: %1$d</string>
<string name="chat_tool_fallback">Tool</string>
<string name="chat_tool_show_raw">Show Raw JSON</string>
<string name="chat_tool_show_parsed">Show Parsed Output</string>
<string name="command_help_desc">Show help menu</string>
<string name="command_status_desc">Check gateway and platform status</string>
<string name="command_sessions_desc">List all chat sessions</string>
<string name="command_stats_desc">Check system resource usage (stats)</string>
<string name="command_system_desc">Check system resource usage (system)</string>
<string name="command_new_desc">Create a new chat session</string>
<string name="command_stop_desc">Interrupt the active run</string>
<string name="content_desc_new_chat">New Chat</string>
<string name="content_desc_scroll_to_bottom">Scroll to bottom</string>
<string name="content_desc_interrupt">Interrupt</string>
<string name="content_desc_copy">Copy</string>
<!-- Settings Screen -->
<string name="settings_sec_connection">Connection</string>
<string name="settings_item_profile">Connection Profile</string>
<string name="settings_profile_default">Standalone / Default</string>
<string name="settings_profile_none">None (Standalone / Default)</string>
<string name="settings_action_rename_profile">Rename Profile</string>
<string name="settings_field_host">Host</string>
<string name="settings_field_port">Port</string>
<string name="settings_field_token">Auth Token</string>
<string name="settings_action_clear_token">Clear Token</string>
<string name="settings_sec_behavior">Behavior</string>
<string name="settings_item_auto_reconnect">Auto-Reconnect</string>
<string name="settings_desc_auto_reconnect">Automatically reconnect on disconnect</string>
<string name="settings_sec_appearance">Appearance</string>
<string name="settings_item_theme">Theme</string>
<string name="settings_sec_chat">Chat</string>
<string name="settings_item_typing_effect">Typing Effect</string>
<string name="settings_desc_typing_effect">Animate new messages word by word</string>
<string name="settings_item_typing_delay">Delay: %1$d ms</string>
<string name="settings_delay_10ms">10 ms</string>
<string name="settings_delay_100ms">100 ms</string>
<string name="settings_sec_nav_bar">Navigation Bar</string>
<string name="settings_item_nav_items">Bottom bar items (%1$d/5)</string>
<string name="settings_action_add_item">Add Item</string>
<string name="settings_nav_max_reached">Maximum of 5 items</string>
<string name="settings_save_success">✅ Settings saved</string>
<string name="settings_action_test_connection">Test Connection</string>
<string name="settings_sec_about">About</string>
<string name="settings_about_app_name">⚡ Hermes Control</string>
<string name="settings_about_version">Version</string>
<string name="settings_about_build">Build</string>
<string name="settings_about_debug">Debug</string>
<string name="settings_about_release">Release</string>
<string name="settings_about_commit">Commit</string>
<string name="action_rename">Rename</string>
<string name="action_save">Save</string>
<string name="content_desc_delete_profile">Delete Profile</string>
<string name="content_desc_hide_token">Hide token</string>
<string name="content_desc_show_token">Show token</string>
<string name="content_desc_move_up">Move up</string>
<string name="content_desc_move_down">Move down</string>
<string name="content_desc_remove_item">Remove %1$s</string>
<string name="theme_system">System</string>
<string name="theme_light">Light</string>
<string name="theme_dark">Dark</string>
<!-- Connect Screen -->
<string name="connect_selected_profile">Profile: %1$s</string>
<string name="connect_action_select_profile">Select Saved Profile</string>
<string name="connect_profile_none">None (Standalone)</string>
<string name="connect_pairing_string">Pairing string</string>
<string name="connect_placeholder_pairing">Paste hermes://\u2026 or Base64 config</string>
<string name="connect_save_profile">Save connection profile</string>
<string name="connect_profile_name">Profile Name</string>
<string name="connect_placeholder_profile_name">e.g. Local Hermit, Remote Server</string>
<string name="connect_action_hide_manual">Hide manual entry</string>
<string name="connect_action_show_manual">Enter manually instead</string>
<string name="connect_placeholder_token">API token</string>
<string name="connect_placeholder_host">127.0.0.1</string>
<string name="connect_placeholder_port">9119</string>
<string name="action_connect">Connect</string>
<!-- Kanban Screen -->
<string name="kanban_board_title">Kanban Board</string>
<string name="kanban_no_boards">No boards found.</string>
<string name="kanban_no_columns">No columns found for this board.</string>
<string name="kanban_add_new_task">Add New Task</string>
<string name="kanban_task_title">Task Title</string>
<string name="kanban_task_desc">Task Description</string>
<string name="action_add">Add</string>
<string name="content_desc_move_left">Move Left</string>
<string name="content_desc_move_right">Move Right</string>
<!-- Skills Screen -->
<string name="skills_status_all">All Statuses</string>
<string name="skills_status_enabled">Enabled</string>
<string name="skills_status_disabled">Disabled</string>
<string name="skills_category_all">All</string>
<string name="skills_no_skills">No skills found</string>
<string name="skills_no_skills_desc">Skills loaded from Hermes will appear here.</string>
<string name="skills_search_placeholder">Search skills\u2026</string>
<string name="skills_filter_status">Status</string>
<string name="skills_filter_category">Category</string>
<string name="skills_no_matching">No matching skills</string>
<string name="skills_no_matching_desc">Try adjusting your search or filters.</string>
<string name="skills_edit_title">Edit: %1$s</string>
<string name="skills_content_placeholder">Enter skill content\u2026</string>
<string name="skills_discard_title">Discard changes?</string>
<string name="skills_discard_message">You have unsaved changes. Are you sure you want to discard them?</string>
<string name="action_discard">Discard</string>
<string name="content_desc_edit_skill">Edit Skill File</string>
<string name="content_desc_save_changes">Save Changes</string>
<!-- Logs Screen -->
<string name="logs_empty_title">No logs available</string>
<string name="logs_empty_desc">Log output from Hermes will appear here.</string>
<!-- System Screen -->
<string name="system_empty_title">No system data</string>
<string name="system_empty_desc">Tap refresh to load system diagnostics.</string>
<string name="system_sec_performance">Performance</string>
<string name="system_metric_cpu">CPU Usage</string>
<string name="system_metric_memory">Memory</string>
<string name="system_sec_doctor">Doctor</string>
<string name="system_status_running">Running</string>
<string name="system_status_failed">Failed</string>
<string name="system_info_pid">Process PID</string>
<string name="system_info_proc_name">Process Name</string>
<string name="system_sec_admin">Administration</string>
<string name="system_action_backup">Trigger System Backup</string>
<!-- Model Screen -->
<string name="model_empty_title">No model providers reported</string>
<string name="model_empty_desc">Verify configuration or retry</string>
<string name="model_label_slug">Slug: %1$s</string>
<string name="model_status_current">CURRENT</string>
<string name="model_label_available">Available Models:</string>
<string name="model_no_models">No models reported or provider needs authentication.</string>
<string name="content_desc_active_model">Active Model</string>
<!-- Sessions (History) Screen -->
<string name="history_empty_title">No history</string>
<string name="history_empty_desc">Previous chat sessions from Hermes will appear here.</string>
<string name="history_untitled">Untitled</string>
<string name="history_message_count">%1$d messages</string>
<!-- Toolsets Screen -->
<string name="toolsets_empty_title">No toolsets reported</string>
<string name="toolsets_empty_desc">Verify configuration or retry</string>
<!-- Webhooks Screen -->
<string name="webhooks_sec_status">Global Webhooks Status</string>
<string name="webhooks_sec_subscriptions">Subscriptions</string>
<string name="webhooks_empty_desc">No webhook subscriptions configured.</string>
<string name="webhooks_label_target">Target URL:</string>
<string name="webhooks_label_events">Events Subscribed:</string>
<!-- Pairing Screen -->
<string name="pairing_screen_title">Device Pairing</string>
<string name="pairing_sec_pending">Pending Approvals</string>
<string name="pairing_action_clear_all">Clear All</string>
<string name="pairing_action_approve">Approve</string>
<string name="pairing_sec_approved">Approved Devices</string>
<string name="pairing_empty_approved">No approved devices paired yet.</string>
<string name="pairing_label_user_id">User ID: %1$s</string>
<string name="pairing_action_revoke">Revoke</string>
<!-- Config Screen -->
<string name="config_screen_title">Configuration</string>
<string name="config_placeholder_yaml">YAML Config...</string>
<string name="config_action_reset">Reset</string>
<string name="config_action_save">Save Config</string>
<!-- Profiles Screen -->
<string name="profiles_empty_title">No profiles available</string>
<string name="profiles_label_model">Model: %1$s</string>
<string name="profiles_label_provider">Provider: %1$s</string>
<string name="profiles_action_edit_soul">Edit Soul</string>
<string name="profiles_action_set_model">Set Model</string>
<string name="profiles_content_desc_active">Active Profile</string>
<!-- Plugins Screen -->
<string name="plugins_empty_title">No plugins found</string>
<string name="plugins_empty_desc">Verify configuration or retry</string>
<string name="plugins_no_desc">No description provided.</string>
<string name="plugins_action_update">Update</string>
<string name="plugins_action_uninstall">Uninstall</string>
<string name="plugins_action_install">Install</string>
<!-- McpServers Screen -->
<string name="mcp_servers_label_transport">Transport: %1$s</string>
<string name="mcp_servers_label_command">Command:</string>
<string name="mcp_servers_action_test">Test</string>
<!-- Gateway Screen -->
<string name="gateway_status_running">GATEWAY RUNNING</string>
<string name="gateway_status_stopped">GATEWAY STOPPED</string>
<string name="gateway_label_version">Version: %1$s</string>
<string name="gateway_sec_controls">Controls</string>
<string name="gateway_action_start">Start</string>
<string name="gateway_action_stop">Stop</string>
<string name="gateway_action_restart">Restart</string>
<string name="gateway_sec_active_platforms">Active Platforms</string>
<!-- CronJobs Screen -->
<string name="cron_empty_title">No cron jobs</string>
<string name="cron_empty_desc">Scheduled tasks from Hermes will appear here.</string>
<string name="cron_status_active">Active</string>
<string name="cron_status_paused">Paused</string>
<string name="cron_action_pause">Pause</string>
<string name="cron_action_resume">Resume</string>
<string name="cron_action_run">Run</string>
<!-- Achievements Screen -->
<string name="achievements_empty_title">No achievements yet</string>
<string name="achievements_empty_desc">Complete tasks to unlock achievements.</string>
<string name="achievements_label_progress">Progress: %1$d / %2$d</string>
<!-- Keys Screen -->
<string name="keys_empty_title">No environment keys reported</string>
<string name="keys_label_not_configured">Not configured</string>
<string name="keys_action_toggle_visibility">Toggle Visibility</string>
<!-- Channels Screen -->
<string name="channels_empty_title">No channels configured</string>
<string name="channels_empty_desc">Add messaging platforms in Hermes to see them here.</string>
<string name="channels_status_active">Active</string>
<string name="channels_status_disabled">Disabled</string>
<string name="channels_action_configure">Configure</string>
<string name="channels_sec_settings">Configuration Settings</string>
<string name="channels_placeholder_configured">******** (Already Configured)</string>
<string name="channels_action_save_settings">Save Settings</string>
<string name="channels_sec_keys">Configured Keys:</string>
<string name="channels_status_configured">Configured</string>
<string name="channels_status_not_set">Not set</string>
</resources>
+5 -3
View File
@@ -29,9 +29,11 @@
# Target platforms (36 required by AGP 9.0.1 / compileSdk 36)
platformVersions = [ "36" "35" "34" ];
# For the emulator (optional, remove if not needed)
includeEmulator = false;
includeSystemImages = false;
# For the emulator
includeEmulator = true;
includeSystemImages = true;
systemImageTypes = [ "google_apis" ];
abiVersions = [ "x86_64" ];
# NDK (not needed for this project, but handy)
includeNDK = false;