fix: resolve all build compilation and style check warnings (#597)

* fix: resolve all build compilation and style check warnings

* fix: remove unused import in SystemViewModel
This commit is contained in:
M57 authored and GitHub committed 2026-07-14 21:55:37 +03:00
1 parent 48f5ffbe52
commit d1fc477bcd
28 files changed
+293 -192

No files matched your search

@@ -25,7 +25,12 @@ internal object DashboardSessionTokenRefresher {
client: OkHttpClient,
): String? =
try {
val request = Request.Builder().url(baseUrl).get().build()
val request =
Request
.Builder()
.url(baseUrl)
.get()
.build()
client.newCall(request).execute().use { response ->
if (!response.isSuccessful) return null
// Cap the read — the SPA HTML can be large, and we only need the
@@ -276,7 +276,7 @@ object HermesWsClient {
}
if (response.isSuccessful) {
val body = response.body?.string() ?: ""
val body = response.body.string()
val ticketMatch = Regex("""\"ticket\":\"([^\"]+)\"""").find(body)
val ticket = ticketMatch?.groupValues?.getOrNull(1)
if (!ticket.isNullOrBlank()) {
@@ -73,26 +73,31 @@ fun AnalyticsScreen(
modifier = modifier,
) {
when {
state.isLoading && state.usage == null -> LoadingState()
state.isLoading && state.usage == null -> {
LoadingState()
}
state.errorMessage != null && state.usage == null ->
state.errorMessage != null && state.usage == null -> {
ErrorState(
message = state.errorMessage ?: stringResource(R.string.error_unknown),
onRetry = { viewModel.load() },
)
}
state.usage == null ->
state.usage == null -> {
EmptyState(
title = stringResource(R.string.analytics_empty_title),
subtitle = stringResource(R.string.analytics_empty_desc),
icon = Icons.Filled.BarChart,
)
}
else ->
else -> {
AnalyticsContent(
state = state,
onDaysSelected = viewModel::setDays,
)
}
}
}
}
@@ -105,13 +105,21 @@ class AnalyticsViewModel : ViewModel() {
}
val error =
when {
usage == null && models == null ->
usage == null && models == null -> {
failure?.message ?: "Failed to load analytics"
usage == null ->
}
usage == null -> {
failure?.message ?: "Failed to load usage analytics"
models == null ->
}
models == null -> {
failure?.message ?: "Failed to load model analytics"
else -> null
}
else -> {
null
}
}
// Only update UI if this is still the active day window.
if (_uiState.value.days == days) {
@@ -228,7 +228,7 @@ class AuthLoginViewModel(
.get()
.build()
val resp = probeClient.newCall(req).execute()
val body = resp.body?.string() ?: ""
val body = resp.body.string()
// Extract __HERMES_SESSION_TOKEN__ from the SPA HTML
val tokenMatch = Regex("""__HERMES_SESSION_TOKEN__\s*=\s*"([^"]+)"""").find(body)
extractedToken = tokenMatch?.groupValues?.getOrNull(1)
@@ -453,7 +453,7 @@ class AuthLoginViewModel(
return null
}
val ticketBody = ticketResp.body?.string() ?: ""
val ticketBody = ticketResp.body.string()
val ticketMatch = Regex(""""ticket":"([^"]+)"""").find(ticketBody)
val ticket = ticketMatch?.groupValues?.getOrNull(1)
@@ -72,9 +72,9 @@ class ChannelsViewModel :
_uiState.update {
it.copy(
isLoading = false,
platforms = data?.platforms.orEmpty(),
gatewayStartCommand = data?.gatewayStartCommand ?: "hermes gateway start",
envPath = data?.envPath ?: "~/.hermes/.env",
platforms = data.platforms.orEmpty(),
gatewayStartCommand = data.gatewayStartCommand,
envPath = data.envPath,
)
}
},
@@ -218,7 +218,7 @@ class ChannelsViewModel :
apiCall = { safeApiCall { ApiClient.hermesApi.testMessagingPlatform(platformId) } },
onStart = {},
onSuccess = { result ->
val msg = result?.message ?: "Test complete"
val msg = result.message
_uiState.update {
it.copy(testingId = null, toastMessage = msg)
}
@@ -297,8 +297,8 @@ class ChannelsViewModel :
it.copy(
onboardingPhase = OnboardingPhase.WAITING,
onboardingSetup = setup,
onboardingQrDataUrl = setup?.qrPayload,
onboardingExpiresIn = formatExpiry(setup?.expiresAt),
onboardingQrDataUrl = setup.qrPayload,
onboardingExpiresIn = formatExpiry(setup.expiresAt),
)
}
@@ -330,7 +330,7 @@ class ChannelsViewModel :
when (result) {
is com.m57.hermescontrol.data.remote.NetworkResult.Success -> {
val status = result.data
if (status?.status == "ready") {
if (status.status == "ready") {
_uiState.update {
it.copy(
onboardingPhase = OnboardingPhase.READY,
@@ -466,7 +466,7 @@ class ChannelsViewModel :
)
}
resetOnboarding()
if (applyResult?.needsRestart == true) {
if (applyResult.needsRestart) {
restartGateway()
} else {
loadPlatforms()
@@ -655,7 +655,7 @@ fun parseToolOutput(
val truncated = if (raw.length > 100) raw.take(100) + "…" else raw
if (truncated.isNotBlank()) "${config.summaryPrefix}$truncated" else null
} else {
null ?: obj
obj
.get("context")
?.takeIf { !it.isJsonNull }
?.asString
@@ -764,7 +764,7 @@ fun parseToolOutput(
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = factSummaryText,
@@ -900,7 +900,7 @@ fun parseToolOutput(
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = ssSummaryText,
@@ -922,13 +922,24 @@ fun parseToolOutput(
val mainOutput =
when {
errorMsg != null -> "❌ $errorMsg"
!success -> "❌ Failed"
usage != null -> "✅ $action in ${target ?: "memory"}\n $usage"
else -> "✅ ${target?.capitalize() ?: "Memory"} $action done"
else -> "✅ ${target?.replaceFirstChar {
if (it.isLowerCase()) {
it.titlecase(
java.util.Locale.getDefault(),
)
} else {
it.toString()
}
} ?: "Memory"} $action done"
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -985,7 +996,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1047,7 +1058,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1081,7 +1092,7 @@ fun parseToolOutput(
}?.joinToString("\n\n") ?: "📚 No skills found"
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1120,7 +1131,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1181,7 +1192,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1229,7 +1240,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1256,7 +1267,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1273,7 +1284,7 @@ fun parseToolOutput(
val success = dataSource.get("success")?.takeIf { !it.isJsonNull }?.asBoolean ?: true
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
val urlSummary = urls?.joinToString(", ") { it.asString?.take(40) ?: "?" } ?: ""
val urlSummary = urls?.joinToString(", ") { it.asString.take(40) } ?: ""
val summaryText = "🕸️ ${urls?.size() ?: 0} page(s)"
val mainOutput =
when {
@@ -1293,7 +1304,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1328,7 +1339,7 @@ fun parseToolOutput(
}?.joinToString("\n\n") ?: "🔍 No matching tools"
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1372,7 +1383,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1410,7 +1421,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1443,7 +1454,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1488,7 +1499,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryText,
@@ -1557,75 +1568,80 @@ fun parseToolOutput(
buildString {
if (errorMsg != null) {
append("❌ $errorMsg")
} else if (multimodalText != null) {
append(multimodalText)
if (textSummary != null && multimodalText != textSummary) {
append("\n\n$textSummary")
}
} else if (summaryText != null) {
append(summaryText)
} else if (textSummary != null) {
append(textSummary)
} else if (appsArr != null) {
appsArr.forEachIndexed { idx, el ->
if (el.isJsonObject) {
val obj = el.asJsonObject
val name =
obj
.get("name")
?.takeIf { !it.isJsonNull }
?.asString ?: "?"
val pid =
obj
.get("pid")
?.takeIf { !it.isJsonNull }
?.asInt
append("${idx + 1}. $name")
if (pid != null) append(" (PID: $pid)")
append("\n")
} else {
val textPart = multimodalText ?: summaryText ?: textSummary
if (textPart != null) {
append(textPart)
if (textPart == multimodalText && textSummary != null && multimodalText != textSummary) {
append("\n\n$textSummary")
}
}
} else if (elementsArr != null) {
append("🖥️ ${elementsArr.size()} interactable elements")
if (mode != null) append(" (mode: $mode)")
if (width != null && height != null) append(" • ${width}x$height")
if (textSummary != null) append("\n\n$textSummary")
// Show first few elements as preview
val previewCount = minOf(elementsArr.size(), 5)
if (previewCount > 0) {
append("\n\n")
for (i in 0 until previewCount) {
val el = elementsArr[i]
if (appsArr != null) {
if (isNotEmpty()) append("\n\n")
appsArr.forEachIndexed { idx, el ->
if (el.isJsonObject) {
val obj = el.asJsonObject
val idx =
val name =
obj
.get("index")
.get("name")
?.takeIf { !it.isJsonNull }
?.asString ?: "?"
val pid =
obj
.get("pid")
?.takeIf { !it.isJsonNull }
?.asInt
val role =
obj
.get("role")
?.takeIf { !it.isJsonNull }
?.asString
val label =
obj
.get("label")
?.takeIf { !it.isJsonNull }
?.asString
append(" #$idx ${role ?: ""}${label?.let { " '$it'" } ?: ""}\n")
append("${idx + 1}. $name")
if (pid != null) append(" (PID: $pid)")
append("\n")
}
}
if (elementsArr.size() > previewCount) {
append(" ... and ${elementsArr.size() - previewCount} more")
} else if (elementsArr != null) {
if (isNotEmpty()) append("\n\n")
append("🖥️ ${elementsArr.size()} interactable elements")
if (mode != null) append(" (mode: $mode)")
if (width != null && height != null) append(" • ${width}x$height")
if (textSummary != null) append("\n\n$textSummary")
// Show first few elements as preview
val previewCount = minOf(elementsArr.size(), 5)
if (previewCount > 0) {
append("\n\n")
for (i in 0 until previewCount) {
val el = elementsArr[i]
if (el.isJsonObject) {
val obj = el.asJsonObject
val idx =
obj
.get("index")
?.takeIf { !it.isJsonNull }
?.asInt
val role =
obj
.get("role")
?.takeIf { !it.isJsonNull }
?.asString
val label =
obj
.get("label")
?.takeIf { !it.isJsonNull }
?.asString
append(" #$idx ${role ?: ""}${label?.let { " '$it'" } ?: ""}\n")
}
}
if (elementsArr.size() > previewCount) {
append(" ... and ${elementsArr.size() - previewCount} more")
}
}
} else if (action != null &&
(multimodalText == null && summaryText == null && textSummary == null)
) {
append("$action:")
if (ok != null) append(" ${if (ok) "✅ ok" else "❌ failed"}")
if (message != null) append("\n$message")
} else if (isEmpty()) {
append("🖥️ Computer use action")
}
} else if (action != null) {
append("$action:")
if (ok != null) append(" ${if (ok) "✅ ok" else "❌ failed"}")
if (message != null) append("\n$message")
} else {
append("🖥️ Computer use action")
}
if (visionAnalysis != null) {
append("\n\n━━━ Vision Analysis ━━━\n$visionAnalysis")
@@ -1633,7 +1649,7 @@ fun parseToolOutput(
}
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
return ParsedToolData(
toolName = resolvedToolName ?: "",
toolName = resolvedToolName,
args = args,
result = dataSource.entrySet().associate { it.key to it.value.toString() },
summaryText = summaryLine,
@@ -1657,8 +1673,16 @@ fun parseToolOutput(
(
dataSource.get("output")?.takeIf { !it.isJsonNull }?.asString
?: listOfNotNull(
dataSource.get("stdout")?.takeIf { !it.isJsonNull }?.asString?.takeIf { it.isNotEmpty() },
dataSource.get("stderr")?.takeIf { !it.isJsonNull }?.asString?.takeIf { it.isNotEmpty() },
dataSource
.get("stdout")
?.takeIf { !it.isJsonNull }
?.asString
?.takeIf { it.isNotEmpty() },
dataSource
.get("stderr")
?.takeIf { !it.isJsonNull }
?.asString
?.takeIf { it.isNotEmpty() },
).joinToString("\n").takeIf { it.isNotEmpty() }
)?.takeIf { it.isNotEmpty() }
val exitCode = (dataSource.get("exit_code") ?: dataSource.get("exitCode"))?.toIntOrNull()
@@ -2331,6 +2355,9 @@ private fun JsonElement?.toIntOrNull(): Int? {
is JsonPrimitive -> {
content.toIntOrNull() ?: content.toDoubleOrNull()?.toInt()
}
else -> null
else -> {
null
}
}
}
@@ -1197,7 +1197,7 @@ private fun SudoPromptDialog(
modifier = Modifier.fillMaxWidth().testTag("sudo_password_input"),
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(autoCorrect = false, keyboardType = KeyboardType.Password),
keyboardOptions = KeyboardOptions(autoCorrectEnabled = false, keyboardType = KeyboardType.Password),
)
}
},
@@ -1247,7 +1247,7 @@ private fun SecretPromptDialog(
modifier = Modifier.fillMaxWidth().testTag("secret_value_input"),
singleLine = true,
visualTransformation = PasswordVisualTransformation(),
keyboardOptions = KeyboardOptions(autoCorrect = false, keyboardType = KeyboardType.Password),
keyboardOptions = KeyboardOptions(autoCorrectEnabled = false, keyboardType = KeyboardType.Password),
)
}
},
@@ -2156,9 +2156,13 @@ private fun ReactionHeartsOverlay(
val emojis =
remember(reactionKind) {
when (reactionKind) {
"ily", "<3", "good bot" ->
"ily", "<3", "good bot" -> {
listOf("💗", "❤️", "💕", "💖", "🩷", "💘", "💝")
else -> listOf("💗", "❤️", "💕", "💖")
}
else -> {
listOf("💗", "❤️", "💕", "💖")
}
}
}
@@ -649,7 +649,7 @@ class ChatViewModel(
_uiState.update {
it.copy(
isLoading = false,
errorMessage = "Error${if (method != null) " ($method)" else ""}: $errorMsg",
errorMessage = "Error ($method): $errorMsg",
)
}
}
@@ -1084,8 +1084,10 @@ class ChatViewModel(
val body = trimmed.removePrefix("/").trimStart()
// Must be exactly "model" with no argument (or just whitespace).
return body.equals("model", ignoreCase = true) ||
body.startsWith("model ", ignoreCase = true) &&
body.substringAfter("model").trim().isEmpty()
(
body.startsWith("model ", ignoreCase = true) &&
body.substringAfter("model").trim().isEmpty()
)
}
/** Preload model options so the picker opens instantly (no spinner on /model). */
@@ -1343,7 +1345,7 @@ class ChatViewModel(
}
}
is NetworkResult.Failure -> Unit
is NetworkResult.Failure -> {}
}
} finally {
isSyncingMessages = false
@@ -1352,7 +1354,10 @@ class ChatViewModel(
}
private suspend fun fetchServerMessageCount(sessionId: String): Int {
val known = _uiState.value.sessions.find { it.id == sessionId }?.messageCount
val known =
_uiState.value.sessions
.find { it.id == sessionId }
?.messageCount
if (known != null) return known
val result =
withContext(Dispatchers.IO) {
@@ -1762,7 +1767,7 @@ class ChatViewModel(
return@launch
}
val body = ticketResp.body?.string().orEmpty()
val body = ticketResp.body.string()
val ticket = JSONObject(body).optString("ticket").takeIf { it.isNotBlank() }
if (ticket.isNullOrBlank()) {
@@ -104,13 +104,16 @@ fun MarkdownText(
Column(modifier = modifier.fillMaxWidth()) {
for (block in blocks) {
when (block) {
is MdBlock.Code -> CodeBlockCard(code = block.code, textColor = textColor)
is MdBlock.Code -> {
CodeBlockCard(code = block.code, textColor = textColor)
}
is MdBlock.Hr ->
is MdBlock.Hr -> {
HorizontalDivider(
modifier = Modifier.padding(vertical = 8.dp),
color = textColor.copy(alpha = 0.25f),
)
}
is MdBlock.Heading -> {
val fontSize =
@@ -291,7 +294,9 @@ fun MarkdownText(
}
}
is MdBlock.Table -> MarkdownTable(block = block, textColor = textColor)
is MdBlock.Table -> {
MarkdownTable(block = block, textColor = textColor)
}
is MdBlock.Footnotes -> {
HorizontalDivider(
@@ -510,7 +515,9 @@ internal fun parseBlocks(src: String): List<MdBlock> {
}
}
line.isBlank() -> i++
line.isBlank() -> {
i++
}
isHorizontalRule(line) -> {
blocks.add(MdBlock.Hr)
@@ -944,19 +951,36 @@ internal fun parseInline(
}
internal sealed interface MdBlock {
data class Code(val code: String) : MdBlock
data class Code(
val code: String,
) : MdBlock
data class Heading(val level: Int, val text: String) : MdBlock
data class Heading(
val level: Int,
val text: String,
) : MdBlock
data class Bullet(val text: String) : MdBlock
data class Bullet(
val text: String,
) : MdBlock
data class Task(val checked: Boolean, val text: String) : MdBlock
data class Task(
val checked: Boolean,
val text: String,
) : MdBlock
data class Ordered(val index: Int, val text: String) : MdBlock
data class Ordered(
val index: Int,
val text: String,
) : MdBlock
data class Quote(val text: String) : MdBlock
data class Quote(
val text: String,
) : MdBlock
data class Paragraph(val text: String) : MdBlock
data class Paragraph(
val text: String,
) : MdBlock
data class Table(
val header: List<String>,
@@ -966,9 +990,13 @@ internal sealed interface MdBlock {
object Hr : MdBlock
data class DefList(val items: List<DefItem>) : MdBlock
data class DefList(
val items: List<DefItem>,
) : MdBlock
data class Footnotes(val notes: List<FnNote>) : MdBlock
data class Footnotes(
val notes: List<FnNote>,
) : MdBlock
}
internal data class DefItem(
@@ -25,6 +25,7 @@ import androidx.compose.material3.CardDefaults
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
@@ -32,7 +33,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.ScrollableTabRow
import androidx.compose.material3.SecondaryScrollableTabRow
import androidx.compose.material3.Switch
import androidx.compose.material3.Tab
import androidx.compose.material3.Text
@@ -494,7 +495,7 @@ private fun ConfigTabs(
selectedCategory: String,
onCategorySelected: (String) -> Unit,
) {
ScrollableTabRow(
SecondaryScrollableTabRow(
selectedTabIndex = categories.indexOf(selectedCategory).coerceAtLeast(0),
edgePadding = 0.dp,
) {
@@ -637,7 +638,6 @@ private fun ConfigField(
is JsonPrimitive -> it.content
is JsonObject -> it.toString()
is JsonArray -> it.toString()
else -> ""
}
} ?: ""
@@ -676,7 +676,7 @@ private fun DropdownField(
modifier =
Modifier
.fillMaxWidth()
.menuAnchor(),
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable),
label = { Text(label) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
singleLine = true,
@@ -190,7 +190,7 @@ class CronJobsViewModel :
}
when (result) {
is NetworkResult.Success -> {
val job = result.data ?: return@launch
val job = result.data
_uiState.update {
it.copy(
editorState =
@@ -34,9 +34,9 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.SecondaryTabRow
import androidx.compose.material3.Switch
import androidx.compose.material3.Tab
import androidx.compose.material3.TabRow
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
@@ -223,7 +223,7 @@ private fun AddServerSection(
colors = CardDefaults.cardColors(containerColor = MaterialTheme.colorScheme.surfaceContainer),
) {
Column(modifier = Modifier.padding(spacing.md)) {
TabRow(selectedTabIndex = if (state.addMode == AddServerMode.HTTP) 0 else 1) {
SecondaryTabRow(selectedTabIndex = if (state.addMode == AddServerMode.HTTP) 0 else 1) {
Tab(
selected = state.addMode == AddServerMode.HTTP,
onClick = { viewModel.setAddMode(AddServerMode.HTTP) },
@@ -562,7 +562,7 @@ private fun CatalogSection(
state.catalogError != null -> {
ErrorState(
message = state.catalogError ?: "",
message = state.catalogError,
onRetry = { viewModel.loadCatalog() },
)
}
@@ -500,7 +500,7 @@ private fun AuxTasksDialog(
if (isAuto) {
"auto (use main model)"
} else {
"${cur?.provider} · ${cur?.model ?: "(provider default)"}"
"${cur.provider} · ${cur.model.ifBlank { "(provider default)" }}"
},
style = MaterialTheme.typography.bodySmall,
color = MaterialTheme.colorScheme.onSurfaceVariant,
@@ -601,7 +601,7 @@ private fun PinnedSectionCard(
pinnedModels.forEach { pinned ->
val isActive =
activeProfile?.provider == pinned.providerSlug &&
activeProfile?.model == pinned.modelName
activeProfile.model == pinned.modelName
Card(
onClick = { onModelClick(pinned.providerSlug, pinned.modelName) },
@@ -778,7 +778,7 @@ private fun ProviderCard(
filteredModels.forEach { model ->
val isActive =
activeProfile?.provider == provider.slug &&
activeProfile?.model == model
activeProfile.model == model
Card(
onClick = { onModelClick(provider.slug, model) },
modifier = Modifier.fillMaxWidth().padding(vertical = 4.dp),
@@ -26,6 +26,7 @@ import androidx.compose.material3.Card
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.ExposedDropdownMenuAnchorType
import androidx.compose.material3.ExposedDropdownMenuBox
import androidx.compose.material3.ExposedDropdownMenuDefaults
import androidx.compose.material3.Icon
@@ -322,7 +323,7 @@ private fun PluginProviderDropdown(
modifier =
Modifier
.fillMaxWidth()
.menuAnchor(),
.menuAnchor(ExposedDropdownMenuAnchorType.PrimaryNotEditable),
label = { Text(label) },
trailingIcon = { ExposedDropdownMenuDefaults.TrailingIcon(expanded = expanded) },
singleLine = true,
@@ -76,30 +76,34 @@ fun ProcessesScreen(
modifier = modifier,
) {
when {
state.sessionId == null ->
state.sessionId == null -> {
EmptyState(
title = stringResource(R.string.processes_no_session_title),
subtitle = stringResource(R.string.processes_no_session_desc),
icon = Icons.Filled.Memory,
)
}
state.isLoading && state.processes.isEmpty() ->
state.isLoading && state.processes.isEmpty() -> {
LoadingState()
}
state.errorMessage != null ->
state.errorMessage != null -> {
ErrorState(
message = state.errorMessage ?: stringResource(R.string.error_unknown),
onRetry = { viewModel.load() },
)
}
state.processes.isEmpty() ->
state.processes.isEmpty() -> {
EmptyState(
title = stringResource(R.string.processes_empty_title),
subtitle = stringResource(R.string.processes_empty_desc),
icon = Icons.Filled.Memory,
)
}
else ->
else -> {
LazyColumn(
modifier = Modifier.fillMaxSize(),
contentPadding = listContentPadding,
@@ -113,6 +117,7 @@ fun ProcessesScreen(
)
}
}
}
}
}
@@ -238,8 +243,14 @@ private fun formatUptime(seconds: Int): String {
val m = seconds / 60
val s = seconds % 60
return when {
m <= 0 -> "${s}s"
m < 60 -> "${m}m ${s}s"
m <= 0 -> {
"${s}s"
}
m < 60 -> {
"${m}m ${s}s"
}
else -> {
val h = m / 60
"${h}h ${m % 60}m"
@@ -332,7 +332,7 @@ class ProfilesViewModel :
_uiState.update {
it.copy(
isSearchingHub = false,
hubSearchResults = result.data?.results.orEmpty(),
hubSearchResults = result.data.results.orEmpty(),
)
}
}
@@ -274,21 +274,25 @@ private fun OAuthFlowDialog(
Text(stringResource(R.string.providers_action_submit_code))
}
}
OAuthFlowPhase.DONE -> {
Button(onClick = onDismissFlow) {
Text(stringResource(R.string.action_done))
}
}
OAuthFlowPhase.ERROR -> {
Button(onClick = onDismissFlow) {
Text(stringResource(R.string.action_close))
}
}
OAuthFlowPhase.SHOW_CLI -> {
Button(onClick = onDismissFlow) {
Text(stringResource(R.string.action_close))
}
}
else -> {
TextButton(onClick = onCancel) {
Text(stringResource(R.string.action_cancel))
@@ -301,8 +305,6 @@ private fun OAuthFlowDialog(
TextButton(onClick = onCancel) {
Text(stringResource(R.string.action_cancel))
}
} else {
Unit
}
},
title = { Text(provider.name, style = MaterialTheme.typography.titleLarge) },
@@ -312,6 +314,7 @@ private fun OAuthFlowDialog(
provider.flow == "external" -> {
ExternalFlowContent(provider = provider)
}
state.flowPhase == OAuthFlowPhase.WAITING_CODE && start?.authUrl != null -> {
PkceFlowContent(
authUrl = start.authUrl,
@@ -321,6 +324,7 @@ private fun OAuthFlowDialog(
error = state.flowErrorMessage,
)
}
state.flowPhase == OAuthFlowPhase.POLLING -> {
DeviceCodeFlowContent(
start = start,
@@ -329,15 +333,17 @@ private fun OAuthFlowDialog(
error = state.flowErrorMessage,
)
}
state.flowPhase == OAuthFlowPhase.DONE -> {
Text(
text = stringResource(R.string.providers_flow_success),
style = MaterialTheme.typography.bodyMedium,
)
}
state.flowErrorMessage != null -> {
Text(
text = state.flowErrorMessage!!,
text = state.flowErrorMessage,
color = MaterialTheme.colorScheme.error,
style = MaterialTheme.typography.bodyMedium,
)
@@ -77,7 +77,7 @@ class ProvidersViewModel :
_uiState.update {
it.copy(
isLoading = false,
providers = data?.providers.orEmpty(),
providers = data.providers.orEmpty(),
)
}
},
@@ -161,16 +161,7 @@ class ProvidersViewModel :
val result = safeApiCall { ApiClient.hermesApi.startOAuthLogin(provider.id) }
when (result) {
is NetworkResult.Success -> {
val start =
result.data ?: run {
_uiState.update {
it.copy(
flowPhase = OAuthFlowPhase.IDLE,
flowErrorMessage = "Empty start response",
)
}
return@launch
}
val start = result.data
_uiState.update {
it.copy(
flowPhase =
@@ -224,13 +215,13 @@ class ProvidersViewModel :
when (result) {
is NetworkResult.Success -> {
val submit = result.data
if (submit?.ok == true) {
if (submit.ok) {
finishFlow("approved")
} else {
_uiState.update {
it.copy(
flowStatus = submit?.status ?: "error",
flowErrorMessage = submit?.message ?: "Submission rejected",
flowStatus = submit.status ?: "error",
flowErrorMessage = submit.message ?: "Submission rejected",
)
}
}
@@ -271,6 +262,7 @@ class ProvidersViewModel :
finishFlow("approved")
return@launch
}
"denied", "expired", "error" -> {
_uiState.update {
it.copy(
@@ -283,6 +275,7 @@ class ProvidersViewModel :
}
return@launch
}
else -> { /* still pending — keep polling */ }
}
}
@@ -28,6 +28,7 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.filled.Send
import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.Code
@@ -37,7 +38,6 @@ import androidx.compose.material.icons.filled.History
import androidx.compose.material.icons.filled.Language
import androidx.compose.material.icons.filled.Search
import androidx.compose.material.icons.filled.SelectAll
import androidx.compose.material.icons.filled.Send
import androidx.compose.material.icons.filled.Terminal
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Button
@@ -115,7 +115,7 @@ import kotlin.math.abs
*/
private fun sourceIcon(source: String?): ImageVector? =
when (source?.lowercase()) {
"telegram", "tg" -> Icons.Filled.Send
"telegram", "tg" -> Icons.AutoMirrored.Filled.Send
"web", "dashboard" -> Icons.Filled.Language
"api", "rest" -> Icons.Filled.Code
"cli", "terminal" -> Icons.Filled.Terminal
@@ -254,10 +254,6 @@ private fun JsonElement.searchText(): String? =
.firstNotNullOfOrNull { fieldName -> this[fieldName]?.searchText() }
?: wrapperFieldNames.firstNotNullOfOrNull { fieldName -> this[fieldName]?.searchText() }
}
else -> {
null
}
}
private fun displayedSessions(state: SessionsUiState): List<SessionTreeItem> =
@@ -85,8 +85,8 @@ class SessionsViewModel : ViewModel() {
it.copy(
isLoading = false,
isLoadingMore = false,
sessions = data?.sessions.orEmpty(),
total = data?.total ?: 0,
sessions = data.sessions.orEmpty(),
total = data.total,
selectedIds = emptySet(),
)
}
@@ -173,7 +173,7 @@ class SessionsViewModel : ViewModel() {
_uiState.update {
it.copy(
isSearching = false,
searchResults = result.data?.results.orEmpty(),
searchResults = result.data.results.orEmpty(),
searchError = null,
)
}
@@ -207,8 +207,7 @@ class SessionsViewModel : ViewModel() {
it.copy(
isLoadingStats = false,
stats =
data?.let { SessionStats(total = it.total, active = it.active) }
?: SessionStats(),
SessionStats(total = data.total, active = data.active),
)
}
},
@@ -318,7 +317,7 @@ class SessionsViewModel : ViewModel() {
}
when (result) {
is NetworkResult.Success -> {
val promptText = result.data?.prompt ?: "No prompt available"
val promptText = result.data.prompt ?: "No prompt available"
_uiState.update { it.copy(toastMessage = promptText) }
}
@@ -138,7 +138,7 @@ class SkillsViewModel(
},
onStart = { _uiState.update { it.copy(isHubSearching = true) } },
onSuccess = { data ->
_uiState.update { it.copy(isHubSearching = false, hubResults = data?.results.orEmpty()) }
_uiState.update { it.copy(isHubSearching = false, hubResults = data.results.orEmpty()) }
},
onError = { errorMsg ->
_uiState.update {
@@ -313,7 +313,7 @@ class SkillsViewModel(
_uiState.update {
it.copy(
isLoadingPreview = false,
previewSkillContent = result.data?.content,
previewSkillContent = result.data.content,
)
}
}
@@ -405,7 +405,7 @@ class SkillsViewModel(
_uiState.update {
it.copy(
isLoadingContent = false,
skillContent = result.data?.content.orEmpty(),
skillContent = result.data.content.orEmpty(),
)
}
}
@@ -1299,6 +1299,7 @@ private fun LazyListScope.operationsSection(
// Debug share results
state.debugShare?.let { share ->
Spacer(modifier = Modifier.height(spacing.sm))
@Suppress("DEPRECATION")
val clipboardManager = LocalClipboardManager.current
Row(
@@ -1637,7 +1638,8 @@ private fun LazyListScope.actionLogSection(
}
private val actionButtonPadding =
androidx.compose.foundation.layout.PaddingValues(horizontal = 8.dp, vertical = 8.dp)
androidx.compose.foundation.layout
.PaddingValues(horizontal = 8.dp, vertical = 8.dp)
@Composable
private fun ActionButtonContent(
@@ -8,7 +8,6 @@ import com.m57.hermescontrol.data.model.ActionResponse
import com.m57.hermescontrol.data.model.ActionStatusResponse
import com.m57.hermescontrol.data.model.CheckpointsResponse
import com.m57.hermescontrol.data.model.CredentialPoolProvider
import com.m57.hermescontrol.data.model.CredentialPoolResponse
import com.m57.hermescontrol.data.model.CuratorResponse
import com.m57.hermescontrol.data.model.DebugShareResponse
import com.m57.hermescontrol.data.model.DoctorResponse
@@ -128,10 +127,7 @@ class SystemViewModel :
curator = (curatorResult as? NetworkResult.Success)?.data,
memory = (memoryResult as? NetworkResult.Success)?.data,
credentials =
(
(credResult as? NetworkResult.Success)?.data
as? CredentialPoolResponse
)?.providers ?: emptyList(),
((credResult as? NetworkResult.Success)?.data)?.providers ?: emptyList(),
checkpoints = (checkpointsResult as? NetworkResult.Success)?.data,
hooks = (hooksResult as? NetworkResult.Success)?.data,
updateInfo = (updateResult as? NetworkResult.Success)?.data,
@@ -414,7 +410,7 @@ class SystemViewModel :
_uiState.update {
it.copy(
toastMessage = "Backup triggered",
backupArchive = result.data?.archive,
backupArchive = result.data.archive,
)
}
loadAll()
@@ -641,7 +637,7 @@ class SystemViewModel :
val result = withContext(Dispatchers.IO) { apiCall() }
when (result) {
is NetworkResult.Success -> {
result.data?.name?.let { pollActionStatus(it) }
result.data.name?.let { pollActionStatus(it) }
_uiState.update { it.copy(toastMessage = "$label started") }
}