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