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:
+6
-1
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user