* feat(#441): add profile cloning and description editing * feat(#441): add profile builder wizard and API enhancements Introduce 5-step Profile Builder wizard into ProfilesScreen and add support for create/clone endpoints. * fix(#441): restore imports and add ProfileInfo constructor defaults * style: run ktlint format on codebase to fix styling errors * fix(#441): address PR review comments including provider slug bug, validation, and strings * fix(#441): remove unused McpServerConfigInput import --------- Co-authored-by: Hermes Agent <hermes@agent.local>
This commit is contained in:
1 parent
846a9380e2
commit
d181851670
37 files changed
+2299
-448
No files matched your search
@@ -6,14 +6,15 @@ data class ProfilesResponse(
|
|||||||
|
|
||||||
data class ProfileInfo(
|
data class ProfileInfo(
|
||||||
val name: String,
|
val name: String,
|
||||||
val path: String?,
|
val path: String? = null,
|
||||||
val is_default: Boolean?,
|
val is_default: Boolean? = null,
|
||||||
val model: String?,
|
val model: String? = null,
|
||||||
val provider: String?,
|
val provider: String? = null,
|
||||||
val has_env: Boolean?,
|
val has_env: Boolean? = null,
|
||||||
val skill_count: Int?,
|
val skill_count: Int? = null,
|
||||||
val gateway_running: Boolean?,
|
val gateway_running: Boolean? = null,
|
||||||
val description: String?,
|
val description: String? = null,
|
||||||
|
val description_auto: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
data class ActiveProfileResponse(
|
data class ActiveProfileResponse(
|
||||||
@@ -37,3 +38,29 @@ data class UpdateProfileModelRequest(
|
|||||||
val provider: String,
|
val provider: String,
|
||||||
val model: String,
|
val model: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class CloneProfileRequest(
|
||||||
|
val name: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class UpdateProfileDescriptionRequest(
|
||||||
|
val description: String,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class CreateProfileRequest(
|
||||||
|
val name: String,
|
||||||
|
val description: String?,
|
||||||
|
val provider: String?,
|
||||||
|
val model: String?,
|
||||||
|
val mcp_servers: List<McpServerConfigInput>?,
|
||||||
|
val keep_skills: Boolean?,
|
||||||
|
val hub_skills: List<String>?,
|
||||||
|
)
|
||||||
|
|
||||||
|
data class McpServerConfigInput(
|
||||||
|
val name: String,
|
||||||
|
val transport: String,
|
||||||
|
val url: String? = null,
|
||||||
|
val command: String? = null,
|
||||||
|
val args: List<String>? = null,
|
||||||
|
)
|
||||||
@@ -7,3 +7,9 @@ data class Skill(
|
|||||||
val enabled: Boolean,
|
val enabled: Boolean,
|
||||||
val content: String? = null,
|
val content: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
data class HubSkill(
|
||||||
|
val name: String,
|
||||||
|
val description: String?,
|
||||||
|
val source: String?,
|
||||||
|
)
|
||||||
@@ -9,9 +9,11 @@ import com.m57.hermescontrol.data.model.AgentPluginInstallBody
|
|||||||
import com.m57.hermescontrol.data.model.AuxiliaryModelsResponse
|
import com.m57.hermescontrol.data.model.AuxiliaryModelsResponse
|
||||||
import com.m57.hermescontrol.data.model.BulkDeleteRequest
|
import com.m57.hermescontrol.data.model.BulkDeleteRequest
|
||||||
import com.m57.hermescontrol.data.model.CheckpointsResponse
|
import com.m57.hermescontrol.data.model.CheckpointsResponse
|
||||||
|
import com.m57.hermescontrol.data.model.CloneProfileRequest
|
||||||
import com.m57.hermescontrol.data.model.ConfigSchemaResponse
|
import com.m57.hermescontrol.data.model.ConfigSchemaResponse
|
||||||
import com.m57.hermescontrol.data.model.ConfigUpdateRequest
|
import com.m57.hermescontrol.data.model.ConfigUpdateRequest
|
||||||
import com.m57.hermescontrol.data.model.CreateCronJobRequest
|
import com.m57.hermescontrol.data.model.CreateCronJobRequest
|
||||||
|
import com.m57.hermescontrol.data.model.CreateProfileRequest
|
||||||
import com.m57.hermescontrol.data.model.CreateTaskBody
|
import com.m57.hermescontrol.data.model.CreateTaskBody
|
||||||
import com.m57.hermescontrol.data.model.CreateWebhookRequest
|
import com.m57.hermescontrol.data.model.CreateWebhookRequest
|
||||||
import com.m57.hermescontrol.data.model.CredentialPoolResponse
|
import com.m57.hermescontrol.data.model.CredentialPoolResponse
|
||||||
@@ -25,6 +27,7 @@ import com.m57.hermescontrol.data.model.EnvVarRevealRequest
|
|||||||
import com.m57.hermescontrol.data.model.EnvVarRevealResponse
|
import com.m57.hermescontrol.data.model.EnvVarRevealResponse
|
||||||
import com.m57.hermescontrol.data.model.EnvVarUpdate
|
import com.m57.hermescontrol.data.model.EnvVarUpdate
|
||||||
import com.m57.hermescontrol.data.model.HookResponse
|
import com.m57.hermescontrol.data.model.HookResponse
|
||||||
|
import com.m57.hermescontrol.data.model.HubSkill
|
||||||
import com.m57.hermescontrol.data.model.KanbanBoardResponse
|
import com.m57.hermescontrol.data.model.KanbanBoardResponse
|
||||||
import com.m57.hermescontrol.data.model.KanbanBoardsResponse
|
import com.m57.hermescontrol.data.model.KanbanBoardsResponse
|
||||||
import com.m57.hermescontrol.data.model.KanbanTask
|
import com.m57.hermescontrol.data.model.KanbanTask
|
||||||
@@ -67,6 +70,7 @@ import com.m57.hermescontrol.data.model.Toolset
|
|||||||
import com.m57.hermescontrol.data.model.ToolsetToggleRequest
|
import com.m57.hermescontrol.data.model.ToolsetToggleRequest
|
||||||
import com.m57.hermescontrol.data.model.UpdateCheckResponse
|
import com.m57.hermescontrol.data.model.UpdateCheckResponse
|
||||||
import com.m57.hermescontrol.data.model.UpdateCronJobRequest
|
import com.m57.hermescontrol.data.model.UpdateCronJobRequest
|
||||||
|
import com.m57.hermescontrol.data.model.UpdateProfileDescriptionRequest
|
||||||
import com.m57.hermescontrol.data.model.UpdateProfileModelRequest
|
import com.m57.hermescontrol.data.model.UpdateProfileModelRequest
|
||||||
import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest
|
import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest
|
||||||
import com.m57.hermescontrol.data.model.UpdateRawConfigRequest
|
import com.m57.hermescontrol.data.model.UpdateRawConfigRequest
|
||||||
@@ -146,6 +150,11 @@ interface HermesApiService {
|
|||||||
@GET("api/skills")
|
@GET("api/skills")
|
||||||
suspend fun getSkills(): Response<List<Skill>>
|
suspend fun getSkills(): Response<List<Skill>>
|
||||||
|
|
||||||
|
@POST("api/skills/hub/search")
|
||||||
|
suspend fun searchSkillsHub(
|
||||||
|
@Query("q") q: String,
|
||||||
|
): Response<List<HubSkill>>
|
||||||
|
|
||||||
@PUT("api/skills/toggle")
|
@PUT("api/skills/toggle")
|
||||||
suspend fun toggleSkill(
|
suspend fun toggleSkill(
|
||||||
@Body body: ToggleSkillRequest,
|
@Body body: ToggleSkillRequest,
|
||||||
@@ -202,6 +211,11 @@ interface HermesApiService {
|
|||||||
@GET("api/profiles")
|
@GET("api/profiles")
|
||||||
suspend fun getProfiles(): Response<ProfilesResponse>
|
suspend fun getProfiles(): Response<ProfilesResponse>
|
||||||
|
|
||||||
|
@POST("api/profiles")
|
||||||
|
suspend fun createProfile(
|
||||||
|
@Body body: CreateProfileRequest,
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
@GET("api/profiles/active")
|
@GET("api/profiles/active")
|
||||||
suspend fun getActiveProfile(): Response<ActiveProfileResponse>
|
suspend fun getActiveProfile(): Response<ActiveProfileResponse>
|
||||||
|
|
||||||
@@ -227,6 +241,18 @@ interface HermesApiService {
|
|||||||
@Body body: UpdateProfileModelRequest,
|
@Body body: UpdateProfileModelRequest,
|
||||||
): Response<Unit>
|
): Response<Unit>
|
||||||
|
|
||||||
|
@POST("api/profiles/{name}/clone")
|
||||||
|
suspend fun cloneProfile(
|
||||||
|
@Path("name") name: String,
|
||||||
|
@Body body: CloneProfileRequest,
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
|
@PUT("api/profiles/{name}/description")
|
||||||
|
suspend fun updateProfileDescription(
|
||||||
|
@Path("name") name: String,
|
||||||
|
@Body body: UpdateProfileDescriptionRequest,
|
||||||
|
): Response<Unit>
|
||||||
|
|
||||||
@GET("api/tools/toolsets")
|
@GET("api/tools/toolsets")
|
||||||
suspend fun getToolsets(): Response<List<Toolset>>
|
suspend fun getToolsets(): Response<List<Toolset>>
|
||||||
|
|
||||||
|
|||||||
@@ -49,7 +49,10 @@ fun mapHttpError(
|
|||||||
val detail =
|
val detail =
|
||||||
if (errorBody != null) {
|
if (errorBody != null) {
|
||||||
try {
|
try {
|
||||||
val parsed = com.google.gson.JsonParser.parseString(errorBody).asJsonObject
|
val parsed =
|
||||||
|
com.google.gson.JsonParser
|
||||||
|
.parseString(errorBody)
|
||||||
|
.asJsonObject
|
||||||
parsed.get("detail")?.asString
|
parsed.get("detail")?.asString
|
||||||
} catch (_: Exception) {
|
} catch (_: Exception) {
|
||||||
null
|
null
|
||||||
|
|||||||
@@ -59,7 +59,11 @@ import com.m57.hermescontrol.ui.common.listItemSpacing
|
|||||||
|
|
||||||
// ── State → badge colour / label ──────────────────────────────────────
|
// ── State → badge colour / label ──────────────────────────────────────
|
||||||
|
|
||||||
private data class StateStyle(val label: String, val color: Color, val icon: ImageVector)
|
private data class StateStyle(
|
||||||
|
val label: String,
|
||||||
|
val color: Color,
|
||||||
|
val icon: ImageVector,
|
||||||
|
)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
private fun platformStateStyle(state: String): StateStyle =
|
private fun platformStateStyle(state: String): StateStyle =
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ data class ChannelsUiState(
|
|||||||
val testingId: String? = null,
|
val testingId: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ChannelsViewModel : ViewModel(), ToastHost {
|
class ChannelsViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(ChannelsUiState())
|
private val _uiState = MutableStateFlow(ChannelsUiState())
|
||||||
val uiState: StateFlow<ChannelsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ChannelsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -537,7 +537,9 @@ fun parseToolOutput(
|
|||||||
val trimmed = content.trim()
|
val trimmed = content.trim()
|
||||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null
|
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null
|
||||||
return try {
|
return try {
|
||||||
val element = com.google.gson.JsonParser.parseString(trimmed)
|
val element =
|
||||||
|
com.google.gson.JsonParser
|
||||||
|
.parseString(trimmed)
|
||||||
if (!element.isJsonObject) return null
|
if (!element.isJsonObject) return null
|
||||||
val obj = element.asJsonObject
|
val obj = element.asJsonObject
|
||||||
|
|
||||||
@@ -566,7 +568,10 @@ 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.get("context")?.takeIf { !it.isJsonNull }?.asString
|
null ?: obj
|
||||||
|
.get("context")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asString
|
||||||
?.takeIf { it.isNotEmpty() }
|
?.takeIf { it.isNotEmpty() }
|
||||||
?.let { "${config.summaryPrefix}$it" }
|
?.let { "${config.summaryPrefix}$it" }
|
||||||
}
|
}
|
||||||
@@ -576,11 +581,13 @@ fun parseToolOutput(
|
|||||||
// build a clean one-line summary and formatted item list.
|
// build a clean one-line summary and formatted item list.
|
||||||
if (resolvedToolName == "todo") {
|
if (resolvedToolName == "todo") {
|
||||||
val todosArray =
|
val todosArray =
|
||||||
dataSource.get("todos")
|
dataSource
|
||||||
|
.get("todos")
|
||||||
?.takeIf { !it.isJsonNull && it.isJsonArray }
|
?.takeIf { !it.isJsonNull && it.isJsonArray }
|
||||||
?.asJsonArray
|
?.asJsonArray
|
||||||
val summaryObj =
|
val summaryObj =
|
||||||
dataSource.get("summary")
|
dataSource
|
||||||
|
.get("summary")
|
||||||
?.takeIf { !it.isJsonNull && it.isJsonObject }
|
?.takeIf { !it.isJsonNull && it.isJsonObject }
|
||||||
?.asJsonObject
|
?.asJsonObject
|
||||||
|
|
||||||
@@ -605,21 +612,23 @@ fun parseToolOutput(
|
|||||||
}
|
}
|
||||||
|
|
||||||
val formattedTodos =
|
val formattedTodos =
|
||||||
todosArray?.mapNotNull { element ->
|
todosArray
|
||||||
if (!element.isJsonObject) return@mapNotNull null
|
?.mapNotNull { element ->
|
||||||
val item = element.asJsonObject
|
if (!element.isJsonObject) return@mapNotNull null
|
||||||
val id = item.get("id")?.asString ?: ""
|
val item = element.asJsonObject
|
||||||
val content = item.get("content")?.asString ?: ""
|
val id = item.get("id")?.asString ?: ""
|
||||||
val status = item.get("status")?.asString ?: "pending"
|
val content = item.get("content")?.asString ?: ""
|
||||||
val marker =
|
val status = item.get("status")?.asString ?: "pending"
|
||||||
when (status) {
|
val marker =
|
||||||
"completed" -> "[x]"
|
when (status) {
|
||||||
"in_progress" -> "[>]"
|
"completed" -> "[x]"
|
||||||
"cancelled" -> "[~]"
|
"in_progress" -> "[>]"
|
||||||
else -> "[ ]"
|
"cancelled" -> "[~]"
|
||||||
}
|
else -> "[ ]"
|
||||||
"$marker $id. $content"
|
}
|
||||||
}?.joinToString("\n")?.takeIf { it.isNotEmpty() }
|
"$marker $id. $content"
|
||||||
|
}?.joinToString("\n")
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||||
|
|
||||||
@@ -645,16 +654,28 @@ fun parseToolOutput(
|
|||||||
|
|
||||||
// Try both key shapes: results[] (probe/search/reason/related/contradict) and facts[] (list)
|
// Try both key shapes: results[] (probe/search/reason/related/contradict) and facts[] (list)
|
||||||
val factsArray =
|
val factsArray =
|
||||||
dataSource.get("results")
|
dataSource
|
||||||
|
.get("results")
|
||||||
?.takeIf { !it.isJsonNull && it.isJsonArray }
|
?.takeIf { !it.isJsonNull && it.isJsonArray }
|
||||||
?.asJsonArray
|
?.asJsonArray
|
||||||
?: dataSource.get("facts")
|
?: dataSource
|
||||||
|
.get("facts")
|
||||||
?.takeIf { !it.isJsonNull && it.isJsonArray }
|
?.takeIf { !it.isJsonNull && it.isJsonArray }
|
||||||
?.asJsonArray
|
?.asJsonArray
|
||||||
|
|
||||||
val count = dataSource.get("count")?.takeIf { !it.isJsonNull }?.asDouble?.toInt() ?: 0
|
val count =
|
||||||
|
dataSource
|
||||||
|
.get("count")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asDouble
|
||||||
|
?.toInt() ?: 0
|
||||||
val status = dataSource.get("status")?.takeIf { !it.isJsonNull }?.asString
|
val status = dataSource.get("status")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val factId = dataSource.get("fact_id")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
val factId =
|
||||||
|
dataSource
|
||||||
|
.get("fact_id")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asDouble
|
||||||
|
?.toInt()
|
||||||
val removed = dataSource.get("removed")?.takeIf { !it.isJsonNull }?.asBoolean
|
val removed = dataSource.get("removed")?.takeIf { !it.isJsonNull }?.asBoolean
|
||||||
val updated = dataSource.get("updated")?.takeIf { !it.isJsonNull }?.asBoolean
|
val updated = dataSource.get("updated")?.takeIf { !it.isJsonNull }?.asBoolean
|
||||||
|
|
||||||
@@ -677,26 +698,28 @@ fun parseToolOutput(
|
|||||||
|
|
||||||
// Format each fact as a compact block: #ID + content on first line, meta on indented second line
|
// Format each fact as a compact block: #ID + content on first line, meta on indented second line
|
||||||
val formattedFacts =
|
val formattedFacts =
|
||||||
factsArray?.mapNotNull { element ->
|
factsArray
|
||||||
if (!element.isJsonObject) return@mapNotNull null
|
?.mapNotNull { element ->
|
||||||
val item = element.asJsonObject
|
if (!element.isJsonObject) return@mapNotNull null
|
||||||
val fid = item.get("fact_id")?.asDouble?.toInt() ?: 0
|
val item = element.asJsonObject
|
||||||
val content = item.get("content")?.asString ?: ""
|
val fid = item.get("fact_id")?.asDouble?.toInt() ?: 0
|
||||||
val category = item.get("category")?.asString
|
val content = item.get("content")?.asString ?: ""
|
||||||
val trust = item.get("trust_score")?.asDouble
|
val category = item.get("category")?.asString
|
||||||
val tags = item.get("tags")?.asString?.takeIf { it.isNotEmpty() }
|
val trust = item.get("trust_score")?.asDouble
|
||||||
|
val tags = item.get("tags")?.asString?.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
val firstLine = "#$fid $content"
|
val firstLine = "#$fid $content"
|
||||||
val metaParts = mutableListOf<String>()
|
val metaParts = mutableListOf<String>()
|
||||||
if (category != null) metaParts.add("[$category]")
|
if (category != null) metaParts.add("[$category]")
|
||||||
if (trust != null) metaParts.add("trust: ${"%.2f".format(trust)}")
|
if (trust != null) metaParts.add("trust: ${"%.2f".format(trust)}")
|
||||||
if (tags != null) metaParts.add("🏷️ $tags")
|
if (tags != null) metaParts.add("🏷️ $tags")
|
||||||
if (metaParts.isNotEmpty()) {
|
if (metaParts.isNotEmpty()) {
|
||||||
"$firstLine\n ${metaParts.joinToString(" ")}"
|
"$firstLine\n ${metaParts.joinToString(" ")}"
|
||||||
} else {
|
} else {
|
||||||
firstLine
|
firstLine
|
||||||
}
|
}
|
||||||
}?.joinToString("\n")?.takeIf { it.isNotEmpty() }
|
}?.joinToString("\n")
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
val factMainOutput =
|
val factMainOutput =
|
||||||
when {
|
when {
|
||||||
@@ -732,8 +755,18 @@ fun parseToolOutput(
|
|||||||
dataSource.get("results")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
dataSource.get("results")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||||
val messagesArray =
|
val messagesArray =
|
||||||
dataSource.get("messages")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
dataSource.get("messages")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||||
val count = dataSource.get("count")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
val count =
|
||||||
val msgCount = dataSource.get("message_count")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
dataSource
|
||||||
|
.get("count")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asDouble
|
||||||
|
?.toInt()
|
||||||
|
val msgCount =
|
||||||
|
dataSource
|
||||||
|
.get("message_count")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asDouble
|
||||||
|
?.toInt()
|
||||||
val truncated = dataSource.get("truncated")?.takeIf { !it.isJsonNull }?.asBoolean
|
val truncated = dataSource.get("truncated")?.takeIf { !it.isJsonNull }?.asBoolean
|
||||||
|
|
||||||
// Build summary line
|
// Build summary line
|
||||||
@@ -744,70 +777,93 @@ fun parseToolOutput(
|
|||||||
60,
|
60,
|
||||||
)}"
|
)}"
|
||||||
} ?: ""}"
|
} ?: ""}"
|
||||||
|
|
||||||
"scroll" -> "📜 ${messagesArray?.size() ?: 0} messages (scroll)"
|
"scroll" -> "📜 ${messagesArray?.size() ?: 0} messages (scroll)"
|
||||||
|
|
||||||
"read" -> "📖 ${msgCount ?: messagesArray?.size() ?: 0} messages${
|
"read" -> "📖 ${msgCount ?: messagesArray?.size() ?: 0} messages${
|
||||||
if (truncated == true) " (truncated)" else ""
|
if (truncated == true) " (truncated)" else ""
|
||||||
}"
|
}"
|
||||||
|
|
||||||
"browse" -> "📋 ${count ?: 0} recent sessions"
|
"browse" -> "📋 ${count ?: 0} recent sessions"
|
||||||
|
|
||||||
else -> "🔍 session_search"
|
else -> "🔍 session_search"
|
||||||
}
|
}
|
||||||
|
|
||||||
// Format results (discover / browse)
|
// Format results (discover / browse)
|
||||||
val formattedResults =
|
val formattedResults =
|
||||||
resultsArray?.mapIndexedNotNull { idx, element ->
|
resultsArray
|
||||||
if (!element.isJsonObject) return@mapIndexedNotNull null
|
?.mapIndexedNotNull { idx, element ->
|
||||||
val item = element.asJsonObject
|
if (!element.isJsonObject) return@mapIndexedNotNull null
|
||||||
val whenField =
|
val item = element.asJsonObject
|
||||||
item.get("when")?.takeIf { !it.isJsonNull }?.asString
|
val whenField =
|
||||||
?: item.get("started_at")?.takeIf { !it.isJsonNull }?.asString
|
item.get("when")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val source = item.get("source")?.takeIf { !it.isJsonNull }?.asString
|
?: item.get("started_at")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val title = item.get("title")?.takeIf { !it.isJsonNull }?.asString
|
val source = item.get("source")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val snippet = item.get("snippet")?.takeIf { !it.isJsonNull }?.asString
|
val title = item.get("title")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val preview = item.get("preview")?.takeIf { !it.isJsonNull }?.asString
|
val snippet = item.get("snippet")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val model = item.get("model")?.takeIf { !it.isJsonNull }?.asString
|
val preview = item.get("preview")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val sessionMsgCount = item.get("message_count")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
val model = item.get("model")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val matchedRole = item.get("matched_role")?.takeIf { !it.isJsonNull }?.asString
|
val sessionMsgCount =
|
||||||
|
item
|
||||||
|
.get("message_count")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asDouble
|
||||||
|
?.toInt()
|
||||||
|
val matchedRole = item.get("matched_role")?.takeIf { !it.isJsonNull }?.asString
|
||||||
|
|
||||||
val header = whenField?.let { "📅 $it" } ?: ""
|
val header = whenField?.let { "📅 $it" } ?: ""
|
||||||
val sourceTag = source?.let { "[$it]" } ?: ""
|
val sourceTag = source?.let { "[$it]" } ?: ""
|
||||||
val titleLine = title?.let { "\n 📄 $it" } ?: ""
|
val titleLine = title?.let { "\n 📄 $it" } ?: ""
|
||||||
val modelLine = model?.let { "\n 🤖 $it" } ?: ""
|
val modelLine = model?.let { "\n 🤖 $it" } ?: ""
|
||||||
val matchedLine = matchedRole?.let { "\n 🎯 matched: $it" } ?: ""
|
val matchedLine = matchedRole?.let { "\n 🎯 matched: $it" } ?: ""
|
||||||
val countLine = sessionMsgCount?.let { "\n $it msgs" } ?: ""
|
val countLine = sessionMsgCount?.let { "\n $it msgs" } ?: ""
|
||||||
val snippetText = snippet ?: preview
|
val snippetText = snippet ?: preview
|
||||||
val snippetLine =
|
val snippetLine =
|
||||||
snippetText?.let {
|
snippetText?.let {
|
||||||
val clean = it.take(200).replace("\n", " ")
|
val clean = it.take(200).replace("\n", " ")
|
||||||
"\n ┃ $clean"
|
"\n ┃ $clean"
|
||||||
} ?: ""
|
} ?: ""
|
||||||
|
|
||||||
"━━━ #${idx + 1} $header$sourceTag$titleLine$modelLine$matchedLine$countLine$snippetLine"
|
"━━━ #${idx + 1} $header$sourceTag$titleLine$modelLine$matchedLine$countLine$snippetLine"
|
||||||
}?.joinToString("\n")?.takeIf { it.isNotEmpty() }
|
}?.joinToString("\n")
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
// Format messages (scroll / read)
|
// Format messages (scroll / read)
|
||||||
val anchorId = dataSource.get("around_message_id")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
val anchorId =
|
||||||
|
dataSource
|
||||||
|
.get("around_message_id")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asDouble
|
||||||
|
?.toInt()
|
||||||
val formattedMessages =
|
val formattedMessages =
|
||||||
messagesArray?.mapNotNull { element ->
|
messagesArray
|
||||||
if (!element.isJsonObject) return@mapNotNull null
|
?.mapNotNull { element ->
|
||||||
val item = element.asJsonObject
|
if (!element.isJsonObject) return@mapNotNull null
|
||||||
val msgId = item.get("id")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
val item = element.asJsonObject
|
||||||
val role = item.get("role")?.takeIf { !it.isJsonNull }?.asString ?: "?"
|
val msgId =
|
||||||
val content = item.get("content")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
item
|
||||||
val toolName = item.get("tool_name")?.takeIf { !it.isJsonNull }?.asString
|
.get("id")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asDouble
|
||||||
|
?.toInt()
|
||||||
|
val role = item.get("role")?.takeIf { !it.isJsonNull }?.asString ?: "?"
|
||||||
|
val content = item.get("content")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||||
|
val toolName = item.get("tool_name")?.takeIf { !it.isJsonNull }?.asString
|
||||||
|
|
||||||
val roleEmoji =
|
val roleEmoji =
|
||||||
when (role) {
|
when (role) {
|
||||||
"user" -> "👤"
|
"user" -> "👤"
|
||||||
"assistant" -> "🤖"
|
"assistant" -> "🤖"
|
||||||
"tool" -> "🔧"
|
"tool" -> "🔧"
|
||||||
else -> "❓"
|
else -> "❓"
|
||||||
}
|
}
|
||||||
val namePart = if (toolName != null) " ($toolName)" else ""
|
val namePart = if (toolName != null) " ($toolName)" else ""
|
||||||
val anchor = if (anchorId != null && msgId == anchorId) " ⬅️" else ""
|
val anchor = if (anchorId != null && msgId == anchorId) " ⬅️" else ""
|
||||||
val cleanContent = content.take(300).replace("\n", " ")
|
val cleanContent = content.take(300).replace("\n", " ")
|
||||||
|
|
||||||
"[$msgId] $roleEmoji $role$namePart$anchor\n $cleanContent"
|
"[$msgId] $roleEmoji $role$namePart$anchor\n $cleanContent"
|
||||||
}?.joinToString("\n")?.takeIf { it.isNotEmpty() }
|
}?.joinToString("\n")
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
|
||||||
val ssMainOutput = formattedResults ?: formattedMessages
|
val ssMainOutput = formattedResults ?: formattedMessages
|
||||||
|
|
||||||
@@ -856,31 +912,46 @@ fun parseToolOutput(
|
|||||||
if (resolvedToolName == "cronjob") {
|
if (resolvedToolName == "cronjob") {
|
||||||
val action = argsObj?.get("action")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
val action = argsObj?.get("action")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||||
val jobsArray = dataSource.get("jobs")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
val jobsArray = dataSource.get("jobs")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||||
val jobId = dataSource.get("job_id")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
val jobId =
|
||||||
|
dataSource
|
||||||
|
.get("job_id")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asDouble
|
||||||
|
?.toInt()
|
||||||
val jobName = dataSource.get("name")?.takeIf { !it.isJsonNull }?.asString
|
val jobName = dataSource.get("name")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
val errorMsg = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString
|
||||||
|
|
||||||
val summaryText = "🔄 $action${jobName?.let { ": $it" } ?: jobId?.let { " (ID: $it)" } ?: ""}"
|
val summaryText = "🔄 $action${jobName?.let { ": $it" } ?: jobId?.let { " (ID: $it)" } ?: ""}"
|
||||||
val mainOutput =
|
val mainOutput =
|
||||||
when {
|
when {
|
||||||
errorMsg != null -> "❌ $errorMsg"
|
errorMsg != null -> {
|
||||||
|
"❌ $errorMsg"
|
||||||
|
}
|
||||||
|
|
||||||
jobsArray != null -> {
|
jobsArray != null -> {
|
||||||
jobsArray.mapNotNull { el ->
|
jobsArray
|
||||||
if (!el.isJsonObject) return@mapNotNull null
|
.mapNotNull { el ->
|
||||||
val j = el.asJsonObject
|
if (!el.isJsonObject) return@mapNotNull null
|
||||||
val name = j.get("name")?.takeIf { !it.isJsonNull }?.asString ?: "?"
|
val j = el.asJsonObject
|
||||||
val schedule = j.get("schedule")?.takeIf { !it.isJsonNull }?.asString
|
val name = j.get("name")?.takeIf { !it.isJsonNull }?.asString ?: "?"
|
||||||
val status = j.get("status")?.takeIf { !it.isJsonNull }?.asString
|
val schedule = j.get("schedule")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val lastRun = j.get("last_run")?.takeIf { !it.isJsonNull }?.asString
|
val status = j.get("status")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val parts = mutableListOf("📌 $name")
|
val lastRun = j.get("last_run")?.takeIf { !it.isJsonNull }?.asString
|
||||||
if (schedule != null) parts.add(" ⏱ $schedule")
|
val parts = mutableListOf("📌 $name")
|
||||||
if (status != null) parts.add(" 📊 $status")
|
if (schedule != null) parts.add(" ⏱ $schedule")
|
||||||
if (lastRun != null) parts.add(" ⏮ $lastRun")
|
if (status != null) parts.add(" 📊 $status")
|
||||||
parts.joinToString("\n")
|
if (lastRun != null) parts.add(" ⏮ $lastRun")
|
||||||
}.joinToString("\n\n")
|
parts.joinToString("\n")
|
||||||
|
}.joinToString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
jobId != null -> {
|
||||||
|
"✅ $action (ID: $jobId)"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
"✅ $action done"
|
||||||
}
|
}
|
||||||
jobId != null -> "✅ $action (ID: $jobId)"
|
|
||||||
else -> "✅ $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(
|
||||||
@@ -900,28 +971,49 @@ 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 webArr =
|
val webArr =
|
||||||
dataSource.get("data")?.takeIf { !it.isJsonNull && it.isJsonObject }
|
dataSource
|
||||||
?.asJsonObject?.get("web")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
.get("data")
|
||||||
|
?.takeIf { !it.isJsonNull && it.isJsonObject }
|
||||||
|
?.asJsonObject
|
||||||
|
?.get("web")
|
||||||
|
?.takeIf { !it.isJsonNull && it.isJsonArray }
|
||||||
|
?.asJsonArray
|
||||||
|
|
||||||
val summaryText = "🌐 $query${webArr?.let { " (${it.size()} results)" } ?: ""}"
|
val summaryText = "🌐 $query${webArr?.let { " (${it.size()} results)" } ?: ""}"
|
||||||
val mainOutput =
|
val mainOutput =
|
||||||
when {
|
when {
|
||||||
errorMsg != null -> "❌ $errorMsg"
|
errorMsg != null -> {
|
||||||
webArr != null ->
|
"❌ $errorMsg"
|
||||||
webArr.mapNotNull { el ->
|
}
|
||||||
if (!el.isJsonObject) return@mapNotNull null
|
|
||||||
val item = el.asJsonObject
|
webArr != null -> {
|
||||||
val title = item.get("title")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
webArr
|
||||||
val url = item.get("url")?.takeIf { !it.isJsonNull }?.asString
|
.mapNotNull { el ->
|
||||||
val desc = item.get("description")?.takeIf { !it.isJsonNull }?.asString
|
if (!el.isJsonObject) return@mapNotNull null
|
||||||
val pos = item.get("position")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
val item = el.asJsonObject
|
||||||
val lines = mutableListOf("${pos?.let { "$it. " } ?: ""}$title")
|
val title = item.get("title")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||||
if (desc != null) lines.add(" $desc")
|
val url = item.get("url")?.takeIf { !it.isJsonNull }?.asString
|
||||||
if (url != null) lines.add(" 🔗 $url")
|
val desc = item.get("description")?.takeIf { !it.isJsonNull }?.asString
|
||||||
lines.joinToString("\n")
|
val pos =
|
||||||
}.joinToString("\n\n")
|
item
|
||||||
!success -> "❌ Search failed"
|
.get("position")
|
||||||
else -> "No results"
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asDouble
|
||||||
|
?.toInt()
|
||||||
|
val lines = mutableListOf("${pos?.let { "$it. " } ?: ""}$title")
|
||||||
|
if (desc != null) lines.add(" $desc")
|
||||||
|
if (url != null) lines.add(" 🔗 $url")
|
||||||
|
lines.joinToString("\n")
|
||||||
|
}.joinToString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
!success -> {
|
||||||
|
"❌ Search failed"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
"No results"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||||
return ParsedToolData(
|
return ParsedToolData(
|
||||||
@@ -940,21 +1032,23 @@ fun parseToolOutput(
|
|||||||
val category = argsObj?.get("category")?.takeIf { !it.isJsonNull }?.asString
|
val category = argsObj?.get("category")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val skillsArr =
|
val skillsArr =
|
||||||
(dataSource.get("skills") ?: dataSource.get("results"))
|
(dataSource.get("skills") ?: dataSource.get("results"))
|
||||||
?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
?.takeIf { !it.isJsonNull && it.isJsonArray }
|
||||||
|
?.asJsonArray
|
||||||
|
|
||||||
val summaryText = "📚 ${skillsArr?.size() ?: 0} skills${category?.let { " ($it)" } ?: ""}"
|
val summaryText = "📚 ${skillsArr?.size() ?: 0} skills${category?.let { " ($it)" } ?: ""}"
|
||||||
val mainOutput =
|
val mainOutput =
|
||||||
skillsArr?.mapNotNull { el ->
|
skillsArr
|
||||||
if (!el.isJsonObject) return@mapNotNull null
|
?.mapNotNull { el ->
|
||||||
val s = el.asJsonObject
|
if (!el.isJsonObject) return@mapNotNull null
|
||||||
val name = s.get("name")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
val s = el.asJsonObject
|
||||||
val desc = s.get("description")?.takeIf { !it.isJsonNull }?.asString
|
val name = s.get("name")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||||
val cat = s.get("category")?.takeIf { !it.isJsonNull }?.asString
|
val desc = s.get("description")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val lines = mutableListOf("📌 $name")
|
val cat = s.get("category")?.takeIf { !it.isJsonNull }?.asString
|
||||||
if (desc != null) lines.add(" $desc")
|
val lines = mutableListOf("📌 $name")
|
||||||
if (cat != null) lines.add(" [$cat]")
|
if (desc != null) lines.add(" $desc")
|
||||||
lines.joinToString("\n")
|
if (cat != null) lines.add(" [$cat]")
|
||||||
}?.joinToString("\n\n") ?: "📚 No skills found"
|
lines.joinToString("\n")
|
||||||
|
}?.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 ?: "",
|
||||||
@@ -1018,27 +1112,42 @@ fun parseToolOutput(
|
|||||||
val summaryText = "⚙️ $action${procId?.let { ": $it" } ?: ""}"
|
val summaryText = "⚙️ $action${procId?.let { ": $it" } ?: ""}"
|
||||||
val mainOutput =
|
val mainOutput =
|
||||||
when {
|
when {
|
||||||
errorMsg != null -> "❌ $errorMsg"
|
errorMsg != null -> {
|
||||||
|
"❌ $errorMsg"
|
||||||
|
}
|
||||||
|
|
||||||
processesArr != null -> {
|
processesArr != null -> {
|
||||||
processesArr.mapNotNull { el ->
|
processesArr
|
||||||
if (!el.isJsonObject) return@mapNotNull null
|
.mapNotNull { el ->
|
||||||
val p = el.asJsonObject
|
if (!el.isJsonObject) return@mapNotNull null
|
||||||
val pid =
|
val p = el.asJsonObject
|
||||||
p.get(
|
val pid =
|
||||||
"session_id",
|
p
|
||||||
)?.takeIf { !it.isJsonNull }?.asString ?: p.get("id")?.asString ?: "?"
|
.get(
|
||||||
val pStatus = p.get("status")?.takeIf { !it.isJsonNull }?.asString
|
"session_id",
|
||||||
val cmd = p.get("command")?.takeIf { !it.isJsonNull }?.asString
|
)?.takeIf { !it.isJsonNull }
|
||||||
val running = p.get("running")?.takeIf { !it.isJsonNull }?.asBoolean
|
?.asString ?: p.get("id")?.asString ?: "?"
|
||||||
val parts = mutableListOf("📌 $pid${cmd?.let { ": $it" } ?: ""}")
|
val pStatus = p.get("status")?.takeIf { !it.isJsonNull }?.asString
|
||||||
if (pStatus != null) parts.add(" Status: $pStatus")
|
val cmd = p.get("command")?.takeIf { !it.isJsonNull }?.asString
|
||||||
if (running != null) parts.add(" Running: $running")
|
val running = p.get("running")?.takeIf { !it.isJsonNull }?.asBoolean
|
||||||
parts.joinToString("\n")
|
val parts = mutableListOf("📌 $pid${cmd?.let { ": $it" } ?: ""}")
|
||||||
}.joinToString("\n\n")
|
if (pStatus != null) parts.add(" Status: $pStatus")
|
||||||
|
if (running != null) parts.add(" Running: $running")
|
||||||
|
parts.joinToString("\n")
|
||||||
|
}.joinToString("\n\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
outputText != null -> {
|
||||||
|
"${status?.let { "Status: $it\n" } ?: ""}$outputText"
|
||||||
|
}
|
||||||
|
|
||||||
|
status != null -> {
|
||||||
|
"✅ $status"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
"✅ $action done"
|
||||||
}
|
}
|
||||||
outputText != null -> "${status?.let { "Status: $it\n" } ?: ""}$outputText"
|
|
||||||
status != null -> "✅ $status"
|
|
||||||
else -> "✅ $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(
|
||||||
@@ -1063,7 +1172,10 @@ fun parseToolOutput(
|
|||||||
val summaryText = "𝕏 $query${if (degraded == true) " (no citations)" else ""}"
|
val summaryText = "𝕏 $query${if (degraded == true) " (no citations)" else ""}"
|
||||||
val mainOutput =
|
val mainOutput =
|
||||||
when {
|
when {
|
||||||
errorMsg != null -> "❌ $errorMsg"
|
errorMsg != null -> {
|
||||||
|
"❌ $errorMsg"
|
||||||
|
}
|
||||||
|
|
||||||
answer != null -> {
|
answer != null -> {
|
||||||
val lines = mutableListOf(answer)
|
val lines = mutableListOf(answer)
|
||||||
if (citationsArr != null && citationsArr.size() > 0) {
|
if (citationsArr != null && citationsArr.size() > 0) {
|
||||||
@@ -1080,7 +1192,10 @@ fun parseToolOutput(
|
|||||||
if (degraded == true) lines.add("\n⚠️ No citations — answer based on model's knowledge")
|
if (degraded == true) lines.add("\n⚠️ No citations — answer based on model's knowledge")
|
||||||
lines.joinToString("\n")
|
lines.joinToString("\n")
|
||||||
}
|
}
|
||||||
else -> "𝕏 No results"
|
|
||||||
|
else -> {
|
||||||
|
"𝕏 No results"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||||
return ParsedToolData(
|
return ParsedToolData(
|
||||||
@@ -1132,12 +1247,19 @@ fun parseToolOutput(
|
|||||||
val summaryText = "🕸️ ${urls?.size() ?: 0} page(s)"
|
val summaryText = "🕸️ ${urls?.size() ?: 0} page(s)"
|
||||||
val mainOutput =
|
val mainOutput =
|
||||||
when {
|
when {
|
||||||
errorMsg != null -> "❌ $errorMsg"
|
errorMsg != null -> {
|
||||||
content != null ->
|
"❌ $errorMsg"
|
||||||
|
}
|
||||||
|
|
||||||
|
content != null -> {
|
||||||
content.take(
|
content.take(
|
||||||
3000,
|
3000,
|
||||||
) + if (content.length > 3000) "\n... [${content.length - 3000} more chars]" else ""
|
) + if (content.length > 3000) "\n... [${content.length - 3000} more chars]" else ""
|
||||||
else -> "🕸️ No content extracted"
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
"🕸️ No content extracted"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||||
return ParsedToolData(
|
return ParsedToolData(
|
||||||
@@ -1154,20 +1276,26 @@ fun parseToolOutput(
|
|||||||
// ── Tool Search-specific formatting ──
|
// ── Tool Search-specific formatting ──
|
||||||
if (resolvedToolName == "tool_search") {
|
if (resolvedToolName == "tool_search") {
|
||||||
val query = argsObj?.get("query")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
val query = argsObj?.get("query")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||||
val limit = argsObj?.get("limit")?.takeIf { !it.isJsonNull }?.asDouble?.toInt()
|
val limit =
|
||||||
|
argsObj
|
||||||
|
?.get("limit")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asDouble
|
||||||
|
?.toInt()
|
||||||
val matchesArr = dataSource.get("matches")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
val matchesArr = dataSource.get("matches")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||||
|
|
||||||
val summaryText = "🔍 $query${matchesArr?.let { " (${it.size()} matches)" } ?: ""}"
|
val summaryText = "🔍 $query${matchesArr?.let { " (${it.size()} matches)" } ?: ""}"
|
||||||
val mainOutput =
|
val mainOutput =
|
||||||
matchesArr?.mapNotNull { el ->
|
matchesArr
|
||||||
if (!el.isJsonObject) return@mapNotNull null
|
?.mapNotNull { el ->
|
||||||
val m = el.asJsonObject
|
if (!el.isJsonObject) return@mapNotNull null
|
||||||
val name = m.get("name")?.takeIf { !it.isJsonNull }?.asString ?: "?"
|
val m = el.asJsonObject
|
||||||
val desc = m.get("description")?.takeIf { !it.isJsonNull }?.asString
|
val name = m.get("name")?.takeIf { !it.isJsonNull }?.asString ?: "?"
|
||||||
val lines = mutableListOf("🔧 $name")
|
val desc = m.get("description")?.takeIf { !it.isJsonNull }?.asString
|
||||||
if (desc != null) lines.add(" $desc")
|
val lines = mutableListOf("🔧 $name")
|
||||||
lines.joinToString("\n")
|
if (desc != null) lines.add(" $desc")
|
||||||
}?.joinToString("\n\n") ?: "🔍 No matching tools"
|
lines.joinToString("\n")
|
||||||
|
}?.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 ?: "",
|
||||||
@@ -1232,20 +1360,21 @@ fun parseToolOutput(
|
|||||||
val summaryText = "📂 ${projectsArr?.size() ?: "?"} projects${activeId?.let { " (1 active)" } ?: ""}"
|
val summaryText = "📂 ${projectsArr?.size() ?: "?"} projects${activeId?.let { " (1 active)" } ?: ""}"
|
||||||
val mainOutput =
|
val mainOutput =
|
||||||
if (projectsArr != null) {
|
if (projectsArr != null) {
|
||||||
projectsArr.mapNotNull { el ->
|
projectsArr
|
||||||
if (!el.isJsonObject) return@mapNotNull null
|
.mapNotNull { el ->
|
||||||
val p = el.asJsonObject
|
if (!el.isJsonObject) return@mapNotNull null
|
||||||
val id = p.get("id")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
val p = el.asJsonObject
|
||||||
val name = p.get("name")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
val id = p.get("id")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||||
val slug = p.get("slug")?.takeIf { !it.isJsonNull }?.asString
|
val name = p.get("name")?.takeIf { !it.isJsonNull }?.asString ?: ""
|
||||||
val path = p.get("primary_path")?.takeIf { !it.isJsonNull }?.asString
|
val slug = p.get("slug")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val isActive = p.get("active")?.takeIf { !it.isJsonNull }?.asBoolean ?: false
|
val path = p.get("primary_path")?.takeIf { !it.isJsonNull }?.asString
|
||||||
val lines = mutableListOf("${if (isActive) "⭐ " else "📁 "}$name")
|
val isActive = p.get("active")?.takeIf { !it.isJsonNull }?.asBoolean ?: false
|
||||||
if (slug != null) lines.add(" 🏷️ $slug")
|
val lines = mutableListOf("${if (isActive) "⭐ " else "📁 "}$name")
|
||||||
if (path != null) lines.add(" 📍 $path")
|
if (slug != null) lines.add(" 🏷️ $slug")
|
||||||
if (isActive) lines.add(" ✅ ACTIVE PROJECT")
|
if (path != null) lines.add(" 📍 $path")
|
||||||
lines.joinToString("\n")
|
if (isActive) lines.add(" ✅ ACTIVE PROJECT")
|
||||||
}.joinToString("\n\n")
|
lines.joinToString("\n")
|
||||||
|
}.joinToString("\n\n")
|
||||||
} else {
|
} else {
|
||||||
""
|
""
|
||||||
}
|
}
|
||||||
@@ -1361,18 +1490,19 @@ fun parseToolOutput(
|
|||||||
if (isMultimodal) {
|
if (isMultimodal) {
|
||||||
val contentArr =
|
val contentArr =
|
||||||
dataSource.get("content")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
dataSource.get("content")?.takeIf { !it.isJsonNull && it.isJsonArray }?.asJsonArray
|
||||||
contentArr?.mapNotNull { el ->
|
contentArr
|
||||||
if (el.isJsonObject) {
|
?.mapNotNull { el ->
|
||||||
val eObj = el.asJsonObject
|
if (el.isJsonObject) {
|
||||||
if (eObj.get("type")?.takeIf { !it.isJsonNull }?.asString == "text") {
|
val eObj = el.asJsonObject
|
||||||
eObj.get("text")?.takeIf { !it.isJsonNull }?.asString
|
if (eObj.get("type")?.takeIf { !it.isJsonNull }?.asString == "text") {
|
||||||
|
eObj.get("text")?.takeIf { !it.isJsonNull }?.asString
|
||||||
|
} else {
|
||||||
|
null
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
} else {
|
}?.joinToString("\n")
|
||||||
null
|
|
||||||
}
|
|
||||||
}?.joinToString("\n")
|
|
||||||
} else {
|
} else {
|
||||||
null
|
null
|
||||||
}
|
}
|
||||||
@@ -1409,8 +1539,16 @@ fun parseToolOutput(
|
|||||||
} else if (appsArr != null) {
|
} else if (appsArr != null) {
|
||||||
appsArr.forEachIndexed { idx, el ->
|
appsArr.forEachIndexed { idx, el ->
|
||||||
if (el.isJsonObject) {
|
if (el.isJsonObject) {
|
||||||
val name = el.asJsonObject.get("name")?.takeIf { !it.isJsonNull }?.asString ?: "?"
|
val name =
|
||||||
val pid = el.asJsonObject.get("pid")?.takeIf { !it.isJsonNull }?.asInt
|
el.asJsonObject
|
||||||
|
.get("name")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asString ?: "?"
|
||||||
|
val pid =
|
||||||
|
el.asJsonObject
|
||||||
|
.get("pid")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asInt
|
||||||
append("${idx + 1}. $name")
|
append("${idx + 1}. $name")
|
||||||
if (pid != null) append(" (PID: $pid)")
|
if (pid != null) append(" (PID: $pid)")
|
||||||
append("\n")
|
append("\n")
|
||||||
@@ -1428,9 +1566,21 @@ fun parseToolOutput(
|
|||||||
for (i in 0 until previewCount) {
|
for (i in 0 until previewCount) {
|
||||||
val el = elementsArr[i]
|
val el = elementsArr[i]
|
||||||
if (el.isJsonObject) {
|
if (el.isJsonObject) {
|
||||||
val idx = el.asJsonObject.get("index")?.takeIf { !it.isJsonNull }?.asInt
|
val idx =
|
||||||
val role = el.asJsonObject.get("role")?.takeIf { !it.isJsonNull }?.asString
|
el.asJsonObject
|
||||||
val label = el.asJsonObject.get("label")?.takeIf { !it.isJsonNull }?.asString
|
.get("index")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asInt
|
||||||
|
val role =
|
||||||
|
el.asJsonObject
|
||||||
|
.get("role")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asString
|
||||||
|
val label =
|
||||||
|
el.asJsonObject
|
||||||
|
.get("label")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asString
|
||||||
append(" #$idx ${role ?: ""}${label?.let { " '$it'" } ?: ""}\n")
|
append(" #$idx ${role ?: ""}${label?.let { " '$it'" } ?: ""}\n")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1467,10 +1617,25 @@ fun parseToolOutput(
|
|||||||
val hasExitCode = dataSource.has("exit_code") || dataSource.has("exitCode")
|
val hasExitCode = dataSource.has("exit_code") || dataSource.has("exitCode")
|
||||||
|
|
||||||
if (hasStdout || hasStderr || hasExitCode) {
|
if (hasStdout || hasStderr || hasExitCode) {
|
||||||
val stdout = dataSource.get("stdout")?.takeIf { !it.isJsonNull }?.asString?.takeIf { it.isNotEmpty() }
|
val stdout =
|
||||||
val stderr = dataSource.get("stderr")?.takeIf { !it.isJsonNull }?.asString?.takeIf { it.isNotEmpty() }
|
dataSource
|
||||||
|
.get("stdout")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asString
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
|
val stderr =
|
||||||
|
dataSource
|
||||||
|
.get("stderr")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asString
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
val exitCode = (dataSource.get("exit_code") ?: dataSource.get("exitCode"))?.takeIf { !it.isJsonNull }?.asInt
|
val exitCode = (dataSource.get("exit_code") ?: dataSource.get("exitCode"))?.takeIf { !it.isJsonNull }?.asInt
|
||||||
val error = dataSource.get("error")?.takeIf { !it.isJsonNull }?.asString?.takeIf { it.isNotEmpty() }
|
val error =
|
||||||
|
dataSource
|
||||||
|
.get("error")
|
||||||
|
?.takeIf { !it.isJsonNull }
|
||||||
|
?.asString
|
||||||
|
?.takeIf { it.isNotEmpty() }
|
||||||
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
val duration = obj.get("duration_s")?.takeIf { !it.isJsonNull }?.asDouble
|
||||||
|
|
||||||
ParsedToolData(
|
ParsedToolData(
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ class ChatPersistenceRepository(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** Load cached messages for a session from Room. */
|
/** Load cached messages for a session from Room. */
|
||||||
suspend fun loadMessages(sessionId: String): List<ChatMessage> {
|
suspend fun loadMessages(sessionId: String): List<ChatMessage> =
|
||||||
return dao.getMessagesForSession(sessionId).map { it.toUiModel() }
|
dao.getMessagesForSession(sessionId).map { it.toUiModel() }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -27,10 +27,14 @@ import com.m57.hermescontrol.R
|
|||||||
/** Sealed interface for navigation icon types — eliminates boolean-flag anti-pattern. */
|
/** Sealed interface for navigation icon types — eliminates boolean-flag anti-pattern. */
|
||||||
sealed interface NavIcon {
|
sealed interface NavIcon {
|
||||||
/** Hamburger menu icon that opens the drawer. */
|
/** Hamburger menu icon that opens the drawer. */
|
||||||
data class Menu(val onOpen: () -> Unit) : NavIcon
|
data class Menu(
|
||||||
|
val onOpen: () -> Unit,
|
||||||
|
) : NavIcon
|
||||||
|
|
||||||
/** Back arrow icon. */
|
/** Back arrow icon. */
|
||||||
data class Back(val onBack: () -> Unit) : NavIcon
|
data class Back(
|
||||||
|
val onBack: () -> Unit,
|
||||||
|
) : NavIcon
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -354,7 +354,9 @@ fun ToastEffect(
|
|||||||
val context = androidx.compose.ui.platform.LocalContext.current
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
androidx.compose.runtime.LaunchedEffect(toastMessage) {
|
androidx.compose.runtime.LaunchedEffect(toastMessage) {
|
||||||
toastMessage?.let { msg ->
|
toastMessage?.let { msg ->
|
||||||
android.widget.Toast.makeText(context, msg, android.widget.Toast.LENGTH_SHORT).show()
|
android.widget.Toast
|
||||||
|
.makeText(context, msg, android.widget.Toast.LENGTH_SHORT)
|
||||||
|
.show()
|
||||||
onClearToast()
|
onClearToast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -336,7 +336,9 @@ private fun FormEditor(
|
|||||||
val isSearching = searchQuery.isNotBlank()
|
val isSearching = searchQuery.isNotBlank()
|
||||||
val categoryCounts =
|
val categoryCounts =
|
||||||
remember(schema) {
|
remember(schema) {
|
||||||
schema.fields.values.groupingBy { it.category ?: "general" }.eachCount()
|
schema.fields.values
|
||||||
|
.groupingBy { it.category ?: "general" }
|
||||||
|
.eachCount()
|
||||||
}
|
}
|
||||||
|
|
||||||
// Non-schema config values — all dot-paths in the config that aren't in the schema
|
// Non-schema config values — all dot-paths in the config that aren't in the schema
|
||||||
@@ -400,17 +402,21 @@ private fun FormEditor(
|
|||||||
remember(schema, activeCategory, searchQuery) {
|
remember(schema, activeCategory, searchQuery) {
|
||||||
if (isSearching) {
|
if (isSearching) {
|
||||||
val query = searchQuery.lowercase()
|
val query = searchQuery.lowercase()
|
||||||
schema.fields.filter { (key, field) ->
|
schema.fields
|
||||||
val label = key.split(".").last().replace("_", " ")
|
.filter { (key, field) ->
|
||||||
key.lowercase().contains(query) ||
|
val label = key.split(".").last().replace("_", " ")
|
||||||
label.contains(query) ||
|
key.lowercase().contains(query) ||
|
||||||
(field.description?.lowercase()?.contains(query) == true) ||
|
label.contains(query) ||
|
||||||
(field.category?.lowercase()?.contains(query) == true)
|
(field.description?.lowercase()?.contains(query) == true) ||
|
||||||
}.entries.map { it.key to it.value }
|
(field.category?.lowercase()?.contains(query) == true)
|
||||||
|
}.entries
|
||||||
|
.map { it.key to it.value }
|
||||||
} else {
|
} else {
|
||||||
schema.fields.filter { (_, field) ->
|
schema.fields
|
||||||
(field.category ?: "general") == activeCategory
|
.filter { (_, field) ->
|
||||||
}.entries.map { it.key to it.value }
|
(field.category ?: "general") == activeCategory
|
||||||
|
}.entries
|
||||||
|
.map { it.key to it.value }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
visibleFields.forEach { (key, field) ->
|
visibleFields.forEach { (key, field) ->
|
||||||
@@ -521,7 +527,12 @@ private fun ConfigField(
|
|||||||
isModified: Boolean,
|
isModified: Boolean,
|
||||||
onChange: (JsonElement) -> Unit,
|
onChange: (JsonElement) -> Unit,
|
||||||
) {
|
) {
|
||||||
val label = key.split(".").last().replace("_", " ").replaceFirstChar { it.uppercase() }
|
val label =
|
||||||
|
key
|
||||||
|
.split(".")
|
||||||
|
.last()
|
||||||
|
.replace("_", " ")
|
||||||
|
.replaceFirstChar { it.uppercase() }
|
||||||
val description = field.description ?: key.replace(".", " → ").replace("_", " ").replaceFirstChar { it.uppercase() }
|
val description = field.description ?: key.replace(".", " → ").replace("_", " ").replaceFirstChar { it.uppercase() }
|
||||||
|
|
||||||
Card(
|
Card(
|
||||||
|
|||||||
@@ -38,7 +38,9 @@ data class ConfigUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ConfigViewModel : ViewModel(), ToastHost {
|
class ConfigViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(ConfigUiState())
|
private val _uiState = MutableStateFlow(ConfigUiState())
|
||||||
val uiState: StateFlow<ConfigUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ConfigUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
@@ -147,6 +149,7 @@ class ConfigViewModel : ViewModel(), ToastHost {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -193,6 +196,7 @@ class ConfigViewModel : ViewModel(), ToastHost {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -231,6 +235,7 @@ class ConfigViewModel : ViewModel(), ToastHost {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ class CronJobsViewModel :
|
|||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Job triggered successfully") }
|
_uiState.update { it.copy(toastMessage = "Job triggered successfully") }
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to trigger cron job: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to trigger cron job: ${result.error.message}") }
|
||||||
}
|
}
|
||||||
@@ -210,6 +211,7 @@ class CronJobsViewModel :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -300,6 +302,7 @@ class CronJobsViewModel :
|
|||||||
closeEditor()
|
closeEditor()
|
||||||
loadCronJobs()
|
loadCronJobs()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -340,7 +343,8 @@ class CronJobsViewModel :
|
|||||||
}
|
}
|
||||||
|
|
||||||
private fun parseLines(skills: String): List<String> =
|
private fun parseLines(skills: String): List<String> =
|
||||||
skills.lines()
|
skills
|
||||||
|
.lines()
|
||||||
.map { it.trim() }
|
.map { it.trim() }
|
||||||
.filter { it.isNotEmpty() }
|
.filter { it.isNotEmpty() }
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ data class GatewayUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class GatewayViewModel : ViewModel(), ToastHost {
|
class GatewayViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(GatewayUiState())
|
private val _uiState = MutableStateFlow(GatewayUiState())
|
||||||
val uiState: StateFlow<GatewayUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<GatewayUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -155,8 +155,7 @@ fun KanbanScreen(
|
|||||||
contentPadding = PaddingValues(16.dp),
|
contentPadding = PaddingValues(16.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
horizontalArrangement = Arrangement.spacedBy(16.dp),
|
||||||
) {
|
) {
|
||||||
items(state.columns.size, key = {
|
items(state.columns.size, key = { index ->
|
||||||
index ->
|
|
||||||
state.columns[index].name
|
state.columns[index].name
|
||||||
}) { columnIndex ->
|
}) { columnIndex ->
|
||||||
val column = state.columns[columnIndex]
|
val column = state.columns[columnIndex]
|
||||||
|
|||||||
@@ -28,7 +28,9 @@ data class KanbanUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class KanbanViewModel : ViewModel(), ToastHost {
|
class KanbanViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(KanbanUiState())
|
private val _uiState = MutableStateFlow(KanbanUiState())
|
||||||
val uiState: StateFlow<KanbanUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<KanbanUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ data class KeysUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class KeysViewModel : ViewModel(), ToastHost {
|
class KeysViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(KeysUiState())
|
private val _uiState = MutableStateFlow(KeysUiState())
|
||||||
val uiState: StateFlow<KeysUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<KeysUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -70,15 +70,25 @@ private enum class LogSeverity {
|
|||||||
private fun detectSeverity(line: String): LogSeverity? {
|
private fun detectSeverity(line: String): LogSeverity? {
|
||||||
val upper = line.uppercase()
|
val upper = line.uppercase()
|
||||||
return when {
|
return when {
|
||||||
Regex("\\[ERROR\\]|\\bERROR\\b|\\|ERROR\\||^ERROR[\\s:]").containsMatchIn(upper) ->
|
Regex("\\[ERROR\\]|\\bERROR\\b|\\|ERROR\\||^ERROR[\\s:]").containsMatchIn(upper) -> {
|
||||||
LogSeverity.ERROR
|
LogSeverity.ERROR
|
||||||
Regex("\\[WARN(?:ING)?\\]|\\bWARN\\b|\\|WARN(?:ING)?\\||^WARN(?:ING)?[\\s:]").containsMatchIn(upper) ->
|
}
|
||||||
|
|
||||||
|
Regex("\\[WARN(?:ING)?\\]|\\bWARN\\b|\\|WARN(?:ING)?\\||^WARN(?:ING)?[\\s:]").containsMatchIn(upper) -> {
|
||||||
LogSeverity.WARN
|
LogSeverity.WARN
|
||||||
Regex("\\[INFO\\]|\\bINFO\\b|\\|INFO\\||^INFO[\\s:]").containsMatchIn(upper) ->
|
}
|
||||||
|
|
||||||
|
Regex("\\[INFO\\]|\\bINFO\\b|\\|INFO\\||^INFO[\\s:]").containsMatchIn(upper) -> {
|
||||||
LogSeverity.INFO
|
LogSeverity.INFO
|
||||||
Regex("\\[DEBUG\\]|\\bDEBUG\\b|\\|DEBUG\\||^DEBUG[\\s:]").containsMatchIn(upper) ->
|
}
|
||||||
|
|
||||||
|
Regex("\\[DEBUG\\]|\\bDEBUG\\b|\\|DEBUG\\||^DEBUG[\\s:]").containsMatchIn(upper) -> {
|
||||||
LogSeverity.DEBUG
|
LogSeverity.DEBUG
|
||||||
else -> null
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
null
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,9 @@ data class LogsUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class LogsViewModel : ViewModel(), ToastHost {
|
class LogsViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(LogsUiState())
|
private val _uiState = MutableStateFlow(LogsUiState())
|
||||||
val uiState: StateFlow<LogsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<LogsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -25,7 +25,9 @@ data class McpServersUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class McpServersViewModel : ViewModel(), ToastHost {
|
class McpServersViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(McpServersUiState())
|
private val _uiState = MutableStateFlow(McpServersUiState())
|
||||||
val uiState: StateFlow<McpServersUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<McpServersUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -305,7 +305,8 @@ private fun ModelSettingsSection(
|
|||||||
) {
|
) {
|
||||||
val auxOverrideCount =
|
val auxOverrideCount =
|
||||||
auxTasks?.let { tasks ->
|
auxTasks?.let { tasks ->
|
||||||
tasks.filterIsInstance<com.m57.hermescontrol.data.model.AuxiliaryTaskAssignment>()
|
tasks
|
||||||
|
.filterIsInstance<com.m57.hermescontrol.data.model.AuxiliaryTaskAssignment>()
|
||||||
.count { a -> a.provider.isNotBlank() && a.provider != "auto" }
|
.count { a -> a.provider.isNotBlank() && a.provider != "auto" }
|
||||||
} ?: 0
|
} ?: 0
|
||||||
|
|
||||||
@@ -700,7 +701,9 @@ private fun MoaConfigDialog(
|
|||||||
}
|
}
|
||||||
|
|
||||||
private sealed class MoaPickerTarget {
|
private sealed class MoaPickerTarget {
|
||||||
data class Reference(val index: Int) : MoaPickerTarget()
|
data class Reference(
|
||||||
|
val index: Int,
|
||||||
|
) : MoaPickerTarget()
|
||||||
|
|
||||||
data object Aggregator : MoaPickerTarget()
|
data object Aggregator : MoaPickerTarget()
|
||||||
}
|
}
|
||||||
@@ -947,14 +950,18 @@ private fun MoaSlotPicker(
|
|||||||
com.m57.hermescontrol.data.model.MoaConfigPreset,
|
com.m57.hermescontrol.data.model.MoaConfigPreset,
|
||||||
) -> com.m57.hermescontrol.data.model.MoaConfigPreset = { preset ->
|
) -> com.m57.hermescontrol.data.model.MoaConfigPreset = { preset ->
|
||||||
when (pickerTarget) {
|
when (pickerTarget) {
|
||||||
is MoaPickerTarget.Aggregator -> preset.copy(aggregator = slot)
|
is MoaPickerTarget.Aggregator -> {
|
||||||
is MoaPickerTarget.Reference ->
|
preset.copy(aggregator = slot)
|
||||||
|
}
|
||||||
|
|
||||||
|
is MoaPickerTarget.Reference -> {
|
||||||
preset.copy(
|
preset.copy(
|
||||||
reference_models =
|
reference_models =
|
||||||
preset.reference_models.toMutableList().also {
|
preset.reference_models.toMutableList().also {
|
||||||
it[pickerTarget.index] = slot
|
it[pickerTarget.index] = slot
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
val preset = draft.presets[selectedPreset]
|
val preset = draft.presets[selectedPreset]
|
||||||
|
|||||||
@@ -152,6 +152,7 @@ class ModelViewModel :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -224,6 +225,7 @@ class ModelViewModel :
|
|||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -265,6 +267,7 @@ class ModelViewModel :
|
|||||||
}
|
}
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -340,6 +343,7 @@ class ModelViewModel :
|
|||||||
}
|
}
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -375,6 +379,7 @@ class ModelViewModel :
|
|||||||
}
|
}
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -417,6 +422,7 @@ class ModelViewModel :
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -450,6 +456,7 @@ class ModelViewModel :
|
|||||||
}
|
}
|
||||||
return@launch
|
return@launch
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
val activeProfileName = activeProfileNameResResult.data.active
|
val activeProfileName = activeProfileNameResResult.data.active
|
||||||
val updateResResult =
|
val updateResResult =
|
||||||
@@ -466,6 +473,7 @@ class ModelViewModel :
|
|||||||
_uiState.update { it.copy(toastMessage = "Successfully set profile model to $modelName") }
|
_uiState.update { it.copy(toastMessage = "Successfully set profile model to $modelName") }
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
|
|||||||
@@ -140,6 +140,7 @@ class PairingCodeEntryViewModel(
|
|||||||
ApiClient.rebuild()
|
ApiClient.rebuild()
|
||||||
_uiState.update { it.copy(isConnecting = false, connectionSuccess = true) }
|
_uiState.update { it.copy(isConnecting = false, connectionSuccess = true) }
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
val msg = buildErrorMessage(result)
|
val msg = buildErrorMessage(result)
|
||||||
setError(msg)
|
setError(msg)
|
||||||
@@ -148,8 +149,8 @@ class PairingCodeEntryViewModel(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun buildErrorMessage(result: NetworkResult.Failure): String {
|
private fun buildErrorMessage(result: NetworkResult.Failure): String =
|
||||||
return when (val err = result.error) {
|
when (val err = result.error) {
|
||||||
is NetworkError.Http -> {
|
is NetworkError.Http -> {
|
||||||
when (err.code) {
|
when (err.code) {
|
||||||
401 -> app.getString(R.string.connect_error_401)
|
401 -> app.getString(R.string.connect_error_401)
|
||||||
@@ -157,13 +158,19 @@ class PairingCodeEntryViewModel(
|
|||||||
else -> app.getString(R.string.connect_error_http_code, err.code)
|
else -> app.getString(R.string.connect_error_http_code, err.code)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
is NetworkError.AuthExpired -> app.getString(R.string.connect_error_401)
|
|
||||||
is NetworkError.Connection ->
|
is NetworkError.AuthExpired -> {
|
||||||
|
app.getString(R.string.connect_error_401)
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkError.Connection -> {
|
||||||
app.getString(R.string.connect_error_connection_failed, err.cause.message ?: "")
|
app.getString(R.string.connect_error_connection_failed, err.cause.message ?: "")
|
||||||
is NetworkError.Unknown ->
|
}
|
||||||
|
|
||||||
|
is NetworkError.Unknown -> {
|
||||||
app.getString(R.string.connect_error_connection_failed, err.cause.message ?: "")
|
app.getString(R.string.connect_error_connection_failed, err.cause.message ?: "")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun setError(msg: String) {
|
private fun setError(msg: String) {
|
||||||
_uiState.update { it.copy(isConnecting = false, errorMessage = msg) }
|
_uiState.update { it.copy(isConnecting = false, errorMessage = msg) }
|
||||||
|
|||||||
@@ -26,7 +26,9 @@ data class PairingUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class PairingViewModel : ViewModel(), ToastHost {
|
class PairingViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(PairingUiState())
|
private val _uiState = MutableStateFlow(PairingUiState())
|
||||||
val uiState: StateFlow<PairingUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<PairingUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -51,7 +51,9 @@ data class PluginsUiState(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class PluginsViewModel : ViewModel(), ToastHost {
|
class PluginsViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(PluginsUiState())
|
private val _uiState = MutableStateFlow(PluginsUiState())
|
||||||
val uiState: StateFlow<PluginsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<PluginsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
@@ -137,6 +139,7 @@ class PluginsViewModel : ViewModel(), ToastHost {
|
|||||||
}
|
}
|
||||||
loadPlugins()
|
loadPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -177,6 +180,7 @@ class PluginsViewModel : ViewModel(), ToastHost {
|
|||||||
_uiState.update { it.copy(providerBusy = false, toastMessage = "Plugin providers saved") }
|
_uiState.update { it.copy(providerBusy = false, toastMessage = "Plugin providers saved") }
|
||||||
loadPlugins()
|
loadPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -201,6 +205,7 @@ class PluginsViewModel : ViewModel(), ToastHost {
|
|||||||
_uiState.update { it.copy(rescanBusy = false, toastMessage = "Plugins rescanned") }
|
_uiState.update { it.copy(rescanBusy = false, toastMessage = "Plugins rescanned") }
|
||||||
loadPlugins()
|
loadPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -257,6 +262,7 @@ class PluginsViewModel : ViewModel(), ToastHost {
|
|||||||
_uiState.update { it.copy(toastMessage = "Plugin enabled successfully") }
|
_uiState.update { it.copy(toastMessage = "Plugin enabled successfully") }
|
||||||
loadPlugins()
|
loadPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to enable plugin: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to enable plugin: ${result.error.message}") }
|
||||||
}
|
}
|
||||||
@@ -289,6 +295,7 @@ class PluginsViewModel : ViewModel(), ToastHost {
|
|||||||
clearRowBusy(name)
|
clearRowBusy(name)
|
||||||
loadPlugins()
|
loadPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to uninstall plugin: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to uninstall plugin: ${result.error.message}") }
|
||||||
clearRowBusy(name)
|
clearRowBusy(name)
|
||||||
@@ -310,6 +317,7 @@ class PluginsViewModel : ViewModel(), ToastHost {
|
|||||||
clearRowBusy(name)
|
clearRowBusy(name)
|
||||||
loadPlugins()
|
loadPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to update plugin: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to update plugin: ${result.error.message}") }
|
||||||
clearRowBusy(name)
|
clearRowBusy(name)
|
||||||
@@ -337,6 +345,7 @@ class PluginsViewModel : ViewModel(), ToastHost {
|
|||||||
clearRowBusy(plugin.name)
|
clearRowBusy(plugin.name)
|
||||||
loadPlugins()
|
loadPlugins()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to update visibility: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to update visibility: ${result.error.message}") }
|
||||||
clearRowBusy(plugin.name)
|
clearRowBusy(plugin.name)
|
||||||
|
|||||||
File diff suppressed because it is too large.
Load diff
@@ -2,8 +2,14 @@ package com.m57.hermescontrol.ui.profiles
|
|||||||
|
|
||||||
import androidx.lifecycle.ViewModel
|
import androidx.lifecycle.ViewModel
|
||||||
import androidx.lifecycle.viewModelScope
|
import androidx.lifecycle.viewModelScope
|
||||||
|
import com.m57.hermescontrol.data.model.CloneProfileRequest
|
||||||
|
import com.m57.hermescontrol.data.model.CreateProfileRequest
|
||||||
|
import com.m57.hermescontrol.data.model.HubSkill
|
||||||
|
import com.m57.hermescontrol.data.model.ModelProvider
|
||||||
import com.m57.hermescontrol.data.model.ProfileInfo
|
import com.m57.hermescontrol.data.model.ProfileInfo
|
||||||
import com.m57.hermescontrol.data.model.SetActiveProfileRequest
|
import com.m57.hermescontrol.data.model.SetActiveProfileRequest
|
||||||
|
import com.m57.hermescontrol.data.model.Skill
|
||||||
|
import com.m57.hermescontrol.data.model.UpdateProfileDescriptionRequest
|
||||||
import com.m57.hermescontrol.data.model.UpdateProfileModelRequest
|
import com.m57.hermescontrol.data.model.UpdateProfileModelRequest
|
||||||
import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest
|
import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest
|
||||||
import com.m57.hermescontrol.data.remote.ApiClient
|
import com.m57.hermescontrol.data.remote.ApiClient
|
||||||
@@ -26,9 +32,17 @@ data class ProfilesUiState(
|
|||||||
val isLoadingSoul: Boolean = false,
|
val isLoadingSoul: Boolean = false,
|
||||||
val errorMessage: String? = null,
|
val errorMessage: String? = null,
|
||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
|
// Profile Builder states
|
||||||
|
val modelProviders: List<ModelProvider> = emptyList(),
|
||||||
|
val isLoadingBuilderData: Boolean = false,
|
||||||
|
val availableSkills: List<Skill> = emptyList(),
|
||||||
|
val hubSearchResults: List<HubSkill> = emptyList(),
|
||||||
|
val isSearchingHub: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ProfilesViewModel : ViewModel(), ToastHost {
|
class ProfilesViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(ProfilesUiState())
|
private val _uiState = MutableStateFlow(ProfilesUiState())
|
||||||
val uiState: StateFlow<ProfilesUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ProfilesUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
@@ -197,4 +211,174 @@ class ProfilesViewModel : ViewModel(), ToastHost {
|
|||||||
fun closeSoulDialog() {
|
fun closeSoulDialog() {
|
||||||
_uiState.update { it.copy(selectedSoulContent = null) }
|
_uiState.update { it.copy(selectedSoulContent = null) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun cloneProfile(
|
||||||
|
sourceProfileName: String,
|
||||||
|
newProfileName: String,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(isLoading = true) }
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall {
|
||||||
|
ApiClient.hermesApi.cloneProfile(
|
||||||
|
sourceProfileName,
|
||||||
|
CloneProfileRequest(newProfileName),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Profile '$sourceProfileName' cloned successfully to '$newProfileName'",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
loadProfiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
toastMessage = "Failed to clone profile '$sourceProfileName': ${result.error.message}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateProfileDescription(
|
||||||
|
profileName: String,
|
||||||
|
description: String,
|
||||||
|
) {
|
||||||
|
viewModelScope.launch {
|
||||||
|
_uiState.update { it.copy(isLoading = true) }
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall {
|
||||||
|
ApiClient.hermesApi.updateProfileDescription(
|
||||||
|
profileName,
|
||||||
|
UpdateProfileDescriptionRequest(description),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Profile '$profileName' description updated",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
loadProfiles()
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
toastMessage = "Failed to update description for '$profileName': ${result.error.message}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun loadBuilderData() {
|
||||||
|
_uiState.update { it.copy(isLoadingBuilderData = true, errorMessage = null) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
val modelsResult =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall { ApiClient.hermesApi.getModelOptions() }
|
||||||
|
}
|
||||||
|
val skillsResult =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall { ApiClient.hermesApi.getSkills() }
|
||||||
|
}
|
||||||
|
|
||||||
|
if (modelsResult is NetworkResult.Success && skillsResult is NetworkResult.Success) {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoadingBuilderData = false,
|
||||||
|
modelProviders = modelsResult.data.providers,
|
||||||
|
availableSkills = skillsResult.data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
val modelsError = (modelsResult as? NetworkResult.Failure)?.error?.message ?: "Success"
|
||||||
|
val skillsError = (skillsResult as? NetworkResult.Failure)?.error?.message ?: "Success"
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoadingBuilderData = false,
|
||||||
|
errorMessage = "Failed to load builder data: Models: $modelsError, Skills: $skillsError",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun searchHub(query: String) {
|
||||||
|
if (query.isBlank()) return
|
||||||
|
_uiState.update { it.copy(isSearchingHub = true) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall { ApiClient.hermesApi.searchSkillsHub(query) }
|
||||||
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isSearchingHub = false,
|
||||||
|
hubSearchResults = result.data,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isSearchingHub = false,
|
||||||
|
toastMessage = "Failed to search skills hub: ${result.error.message}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun createProfile(
|
||||||
|
request: CreateProfileRequest,
|
||||||
|
onSuccess: () -> Unit,
|
||||||
|
) {
|
||||||
|
_uiState.update { it.copy(isLoading = true) }
|
||||||
|
viewModelScope.launch {
|
||||||
|
val result =
|
||||||
|
withContext(Dispatchers.IO) {
|
||||||
|
safeApiCall { ApiClient.hermesApi.createProfile(request) }
|
||||||
|
}
|
||||||
|
when (result) {
|
||||||
|
is NetworkResult.Success -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
toastMessage = "Profile ${request.name} created successfully",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
loadProfiles()
|
||||||
|
onSuccess()
|
||||||
|
}
|
||||||
|
|
||||||
|
is NetworkResult.Failure -> {
|
||||||
|
_uiState.update {
|
||||||
|
it.copy(
|
||||||
|
isLoading = false,
|
||||||
|
toastMessage = "Failed to create profile: ${result.error.message}",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +245,8 @@ fun SessionsScreen(
|
|||||||
val sessionTitle =
|
val sessionTitle =
|
||||||
state.sessions
|
state.sessions
|
||||||
.find { it.id == sessionToDelete }
|
.find { it.id == sessionToDelete }
|
||||||
?.title?.takeIf { it.isNotBlank() } ?: stringResource(R.string.history_untitled)
|
?.title
|
||||||
|
?.takeIf { it.isNotBlank() } ?: stringResource(R.string.history_untitled)
|
||||||
AlertDialog(
|
AlertDialog(
|
||||||
onDismissRequest = { viewModel.cancelDeleteSession() },
|
onDismissRequest = { viewModel.cancelDeleteSession() },
|
||||||
title = { Text(stringResource(R.string.sessions_delete_title)) },
|
title = { Text(stringResource(R.string.sessions_delete_title)) },
|
||||||
|
|||||||
@@ -157,12 +157,14 @@ fun SettingsScreen(
|
|||||||
|
|
||||||
Spacer(modifier = Modifier.height(16.dp))
|
Spacer(modifier = Modifier.height(16.dp))
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsTab.BEHAVIOR -> {
|
SettingsTab.BEHAVIOR -> {
|
||||||
BehaviorSection(
|
BehaviorSection(
|
||||||
autoReconnect = state.autoReconnect,
|
autoReconnect = state.autoReconnect,
|
||||||
onAutoReconnectChange = viewModel::onAutoReconnectChange,
|
onAutoReconnectChange = viewModel::onAutoReconnectChange,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsTab.APPEARANCE -> {
|
SettingsTab.APPEARANCE -> {
|
||||||
AppearanceSection(
|
AppearanceSection(
|
||||||
themePreference = state.themePreference,
|
themePreference = state.themePreference,
|
||||||
@@ -190,6 +192,7 @@ fun SettingsScreen(
|
|||||||
onAddNavItem = viewModel::addNavItem,
|
onAddNavItem = viewModel::addNavItem,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
SettingsTab.ABOUT -> {
|
SettingsTab.ABOUT -> {
|
||||||
// ── About section ─────────────────────────────────────────
|
// ── About section ─────────────────────────────────────────
|
||||||
SectionCard(title = stringResource(R.string.settings_sec_about)) {
|
SectionCard(title = stringResource(R.string.settings_sec_about)) {
|
||||||
@@ -876,30 +879,41 @@ private fun AppearanceSection(
|
|||||||
text = {
|
text = {
|
||||||
Text(
|
Text(
|
||||||
when (preset) {
|
when (preset) {
|
||||||
ThemePreset.DEFAULT ->
|
ThemePreset.DEFAULT -> {
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.theme_preset_default,
|
R.string.theme_preset_default,
|
||||||
)
|
)
|
||||||
ThemePreset.MONOCHROME ->
|
}
|
||||||
|
|
||||||
|
ThemePreset.MONOCHROME -> {
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.theme_preset_monochrome,
|
R.string.theme_preset_monochrome,
|
||||||
)
|
)
|
||||||
ThemePreset.GRUVBOX ->
|
}
|
||||||
|
|
||||||
|
ThemePreset.GRUVBOX -> {
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.theme_preset_gruvbox,
|
R.string.theme_preset_gruvbox,
|
||||||
)
|
)
|
||||||
ThemePreset.CATPPUCCIN ->
|
}
|
||||||
|
|
||||||
|
ThemePreset.CATPPUCCIN -> {
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.theme_preset_catppuccin,
|
R.string.theme_preset_catppuccin,
|
||||||
)
|
)
|
||||||
ThemePreset.AMOLED ->
|
}
|
||||||
|
|
||||||
|
ThemePreset.AMOLED -> {
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.theme_preset_amoled,
|
R.string.theme_preset_amoled,
|
||||||
)
|
)
|
||||||
ThemePreset.NEON_NOIR ->
|
}
|
||||||
|
|
||||||
|
ThemePreset.NEON_NOIR -> {
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.theme_preset_neon_noir,
|
R.string.theme_preset_neon_noir,
|
||||||
)
|
)
|
||||||
|
}
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -88,14 +88,13 @@ import com.m57.hermescontrol.ui.common.ToastEffect
|
|||||||
|
|
||||||
// ── Helpers ─────────────────────────────────────────────────────────────
|
// ── Helpers ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
private fun formatBytes(bytes: Long): String {
|
private fun formatBytes(bytes: Long): String =
|
||||||
return when {
|
when {
|
||||||
bytes < 1024 -> "$bytes B"
|
bytes < 1024 -> "$bytes B"
|
||||||
bytes < 1024 * 1024 -> "${bytes / 1024} KB"
|
bytes < 1024 * 1024 -> "${bytes / 1024} KB"
|
||||||
bytes < 1024 * 1024 * 1024 -> "${bytes / (1024 * 1024)} MB"
|
bytes < 1024 * 1024 * 1024 -> "${bytes / (1024 * 1024)} MB"
|
||||||
else -> "${"%.1f".format(bytes.toDouble() / (1024 * 1024 * 1024))} GB"
|
else -> "${"%.1f".format(bytes.toDouble() / (1024 * 1024 * 1024))} GB"
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
private fun formatDuration(totalSeconds: Double): String {
|
private fun formatDuration(totalSeconds: Double): String {
|
||||||
val days = (totalSeconds / 86400).toInt()
|
val days = (totalSeconds / 86400).toInt()
|
||||||
@@ -341,8 +340,7 @@ private fun LazyListScope.hostSection(
|
|||||||
stats.os?.let { InfoRow(stringResource(R.string.system_label_os), it) }
|
stats.os?.let { InfoRow(stringResource(R.string.system_label_os), it) }
|
||||||
stats.arch?.let { InfoRow(stringResource(R.string.system_label_arch), it) }
|
stats.arch?.let { InfoRow(stringResource(R.string.system_label_arch), it) }
|
||||||
stats.hostname?.let { InfoRow(stringResource(R.string.system_label_hostname), it) }
|
stats.hostname?.let { InfoRow(stringResource(R.string.system_label_hostname), it) }
|
||||||
stats.python_version?.let {
|
stats.python_version?.let { python ->
|
||||||
python ->
|
|
||||||
val impl = stats.python_impl?.let { " ($it)" } ?: ""
|
val impl = stats.python_impl?.let { " ($it)" } ?: ""
|
||||||
InfoRow(stringResource(R.string.system_label_python), "$python$impl")
|
InfoRow(stringResource(R.string.system_label_python), "$python$impl")
|
||||||
}
|
}
|
||||||
@@ -352,14 +350,21 @@ private fun LazyListScope.hostSection(
|
|||||||
val updateBadge =
|
val updateBadge =
|
||||||
state.updateInfo?.let { info ->
|
state.updateInfo?.let { info ->
|
||||||
when {
|
when {
|
||||||
info.update_available == true && info.behind != null && info.behind > 0 ->
|
info.update_available == true && info.behind != null && info.behind > 0 -> {
|
||||||
" (${stringResource(
|
" (${stringResource(
|
||||||
R.string.system_version_update_available,
|
R.string.system_version_update_available,
|
||||||
)}: ${stringResource(R.string.system_version_behind, info.behind)})"
|
)}: ${stringResource(R.string.system_version_behind, info.behind)})"
|
||||||
info.update_available == true -> " (${stringResource(
|
}
|
||||||
R.string.system_version_update_available,
|
|
||||||
)})"
|
info.update_available == true -> {
|
||||||
else -> " (${stringResource(R.string.system_version_latest)})"
|
" (${stringResource(
|
||||||
|
R.string.system_version_update_available,
|
||||||
|
)})"
|
||||||
|
}
|
||||||
|
|
||||||
|
else -> {
|
||||||
|
" (${stringResource(R.string.system_version_latest)})"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} ?: ""
|
} ?: ""
|
||||||
InfoRow(stringResource(R.string.system_label_hermes_version), "$ver$updateBadge")
|
InfoRow(stringResource(R.string.system_label_hermes_version), "$ver$updateBadge")
|
||||||
@@ -620,10 +625,12 @@ private fun LazyListScope.curatorSection(
|
|||||||
statusText = stringResource(R.string.system_status_disabled)
|
statusText = stringResource(R.string.system_status_disabled)
|
||||||
statusType = StatusBadgeType.ERROR
|
statusType = StatusBadgeType.ERROR
|
||||||
}
|
}
|
||||||
|
|
||||||
curator.paused == true -> {
|
curator.paused == true -> {
|
||||||
statusText = stringResource(R.string.system_status_paused)
|
statusText = stringResource(R.string.system_status_paused)
|
||||||
statusType = StatusBadgeType.WARNING
|
statusType = StatusBadgeType.WARNING
|
||||||
}
|
}
|
||||||
|
|
||||||
else -> {
|
else -> {
|
||||||
statusText = stringResource(R.string.system_status_active)
|
statusText = stringResource(R.string.system_status_active)
|
||||||
statusType = StatusBadgeType.SUCCESS
|
statusType = StatusBadgeType.SUCCESS
|
||||||
|
|||||||
@@ -77,7 +77,9 @@ data class SystemUiState(
|
|||||||
val updateConfirmOpen: Boolean = false,
|
val updateConfirmOpen: Boolean = false,
|
||||||
)
|
)
|
||||||
|
|
||||||
class SystemViewModel : ViewModel(), ToastHost {
|
class SystemViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
companion object {
|
companion object {
|
||||||
private const val TAG = "SystemViewModel"
|
private const val TAG = "SystemViewModel"
|
||||||
}
|
}
|
||||||
@@ -186,6 +188,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
_uiState.update { it.copy(toastMessage = "Gateway ${name}ed successfully") }
|
_uiState.update { it.copy(toastMessage = "Gateway ${name}ed successfully") }
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to $name gateway: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to $name gateway: ${result.error.message}") }
|
||||||
}
|
}
|
||||||
@@ -206,6 +209,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(checkingUpdate = false, updateInfo = result.data) }
|
_uiState.update { it.copy(checkingUpdate = false, updateInfo = result.data) }
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -251,6 +255,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
}
|
}
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to toggle curator: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to toggle curator: ${result.error.message}") }
|
||||||
}
|
}
|
||||||
@@ -278,6 +283,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
_uiState.update { it.copy(toastMessage = "Memory ($target) reset successfully") }
|
_uiState.update { it.copy(toastMessage = "Memory ($target) reset successfully") }
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to reset memory: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to reset memory: ${result.error.message}") }
|
||||||
}
|
}
|
||||||
@@ -329,6 +335,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
}
|
}
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(addingCred = false, toastMessage = "Failed to add credential: ${result.error.message}")
|
it.copy(addingCred = false, toastMessage = "Failed to add credential: ${result.error.message}")
|
||||||
@@ -352,6 +359,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
_uiState.update { it.copy(toastMessage = "Credential removed") }
|
_uiState.update { it.copy(toastMessage = "Credential removed") }
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to remove credential: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to remove credential: ${result.error.message}") }
|
||||||
}
|
}
|
||||||
@@ -411,6 +419,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
}
|
}
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to trigger backup: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to trigger backup: ${result.error.message}") }
|
||||||
}
|
}
|
||||||
@@ -433,6 +442,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
is NetworkResult.Success -> {
|
is NetworkResult.Success -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Backup downloaded") }
|
_uiState.update { it.copy(toastMessage = "Backup downloaded") }
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to download backup: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to download backup: ${result.error.message}") }
|
||||||
}
|
}
|
||||||
@@ -475,6 +485,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -556,6 +567,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
}
|
}
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -582,6 +594,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
_uiState.update { it.copy(toastMessage = "Hook deleted") }
|
_uiState.update { it.copy(toastMessage = "Hook deleted") }
|
||||||
loadAll()
|
loadAll()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to delete hook: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "Failed to delete hook: ${result.error.message}") }
|
||||||
}
|
}
|
||||||
@@ -631,6 +644,7 @@ class SystemViewModel : ViewModel(), ToastHost {
|
|||||||
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") }
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update { it.copy(toastMessage = "$label failed: ${result.error.message}") }
|
_uiState.update { it.copy(toastMessage = "$label failed: ${result.error.message}") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,9 @@ data class ToolsetsUiState(
|
|||||||
val toastMessage: String? = null,
|
val toastMessage: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class ToolsetsViewModel : ViewModel(), ToastHost {
|
class ToolsetsViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(ToolsetsUiState())
|
private val _uiState = MutableStateFlow(ToolsetsUiState())
|
||||||
val uiState: StateFlow<ToolsetsUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<ToolsetsUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
|
|||||||
@@ -42,7 +42,9 @@ data class WebhooksUiState(
|
|||||||
val togglingName: String? = null,
|
val togglingName: String? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
class WebhooksViewModel : ViewModel(), ToastHost {
|
class WebhooksViewModel :
|
||||||
|
ViewModel(),
|
||||||
|
ToastHost {
|
||||||
private val _uiState = MutableStateFlow(WebhooksUiState())
|
private val _uiState = MutableStateFlow(WebhooksUiState())
|
||||||
val uiState: StateFlow<WebhooksUiState> = _uiState.asStateFlow()
|
val uiState: StateFlow<WebhooksUiState> = _uiState.asStateFlow()
|
||||||
|
|
||||||
@@ -185,6 +187,7 @@ class WebhooksViewModel : ViewModel(), ToastHost {
|
|||||||
}
|
}
|
||||||
loadWebhooks()
|
loadWebhooks()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(
|
it.copy(
|
||||||
@@ -225,6 +228,7 @@ class WebhooksViewModel : ViewModel(), ToastHost {
|
|||||||
}
|
}
|
||||||
loadWebhooks()
|
loadWebhooks()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(toastMessage = "Failed to toggle \"$name\": ${result.error.message}")
|
it.copy(toastMessage = "Failed to toggle \"$name\": ${result.error.message}")
|
||||||
@@ -261,6 +265,7 @@ class WebhooksViewModel : ViewModel(), ToastHost {
|
|||||||
}
|
}
|
||||||
loadWebhooks()
|
loadWebhooks()
|
||||||
}
|
}
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
is NetworkResult.Failure -> {
|
||||||
_uiState.update {
|
_uiState.update {
|
||||||
it.copy(toastMessage = "Failed to delete \"${target.name}\": ${result.error.message}")
|
it.copy(toastMessage = "Failed to delete \"${target.name}\": ${result.error.message}")
|
||||||
|
|||||||
@@ -505,14 +505,70 @@
|
|||||||
<string name="profiles_empty_desc">Connection profiles from Hermes will appear here.</string>
|
<string name="profiles_empty_desc">Connection profiles from Hermes will appear here.</string>
|
||||||
<string name="profiles_label_model">Model: %1$s</string>
|
<string name="profiles_label_model">Model: %1$s</string>
|
||||||
<string name="profiles_label_provider">Provider: %1$s</string>
|
<string name="profiles_label_provider">Provider: %1$s</string>
|
||||||
|
<string name="profiles_label_skills">%1$d skills</string>
|
||||||
|
<string name="profiles_label_path">Path: %1$s</string>
|
||||||
|
<string name="profiles_badge_auto_generated">Auto-generated</string>
|
||||||
<string name="profiles_action_activate">Activate</string>
|
<string name="profiles_action_activate">Activate</string>
|
||||||
<string name="profiles_action_edit_soul">Edit Soul</string>
|
<string name="profiles_action_edit_soul">Edit Soul</string>
|
||||||
<string name="profiles_action_set_model">Set Model</string>
|
<string name="profiles_action_set_model">Set Model</string>
|
||||||
|
<string name="profiles_action_clone">Clone</string>
|
||||||
|
<string name="profiles_action_edit_description">Edit Description</string>
|
||||||
<string name="profiles_content_desc_active">Active Profile</string>
|
<string name="profiles_content_desc_active">Active Profile</string>
|
||||||
<string name="profiles_title_edit_soul">Edit Soul Profile: %1$s</string>
|
<string name="profiles_title_edit_soul">Edit Soul Profile: %1$s</string>
|
||||||
<string name="profiles_title_set_model">Set Model: %1$s</string>
|
<string name="profiles_title_set_model">Set Model: %1$s</string>
|
||||||
|
<string name="profiles_title_clone">Clone Profile: %1$s</string>
|
||||||
|
<string name="profiles_title_edit_description">Edit Description: %1$s</string>
|
||||||
<string name="profiles_label_provider_input">Provider</string>
|
<string name="profiles_label_provider_input">Provider</string>
|
||||||
<string name="profiles_label_model_input">Model Name</string>
|
<string name="profiles_label_model_input">Model Name</string>
|
||||||
|
<string name="profiles_label_new_name_input">New Profile Name</string>
|
||||||
|
<string name="profiles_label_description_input">Description</string>
|
||||||
|
<string name="profiles_description_placeholder">No description provided. Describe this profile\'s role.</string>
|
||||||
|
<string name="profiles_builder_step_indicator">Step %1$d of 5</string>
|
||||||
|
<string name="profiles_builder_step_identity">Identity</string>
|
||||||
|
<string name="profiles_builder_step_model">Model Configuration</string>
|
||||||
|
<string name="profiles_builder_step_skills">Skills selection</string>
|
||||||
|
<string name="profiles_builder_step_mcp">MCP Servers</string>
|
||||||
|
<string name="profiles_builder_step_review">Review & Create</string>
|
||||||
|
<string name="profiles_builder_action_back">Back</string>
|
||||||
|
<string name="profiles_builder_action_next">Next</string>
|
||||||
|
<string name="profiles_builder_action_create">Create Profile</string>
|
||||||
|
<string name="profiles_builder_label_profile_name">Profile Name</string>
|
||||||
|
<string name="profiles_builder_label_description">Description</string>
|
||||||
|
<string name="profiles_builder_title_provider">Select Provider (Optional)</string>
|
||||||
|
<string name="profiles_builder_title_model">Select Model</string>
|
||||||
|
<string name="profiles_builder_skills_default">Include default skills bundle</string>
|
||||||
|
<string name="profiles_builder_skills_default_desc">Automatically bundles common system tools</string>
|
||||||
|
<string name="profiles_builder_skills_local">Select Local Skills</string>
|
||||||
|
<string name="profiles_builder_skills_search_hub">Search Hub</string>
|
||||||
|
<string name="profiles_builder_skills_hub_title">Add Skills from Hermes Hub</string>
|
||||||
|
<string name="profiles_builder_skills_added">Added Skills:</string>
|
||||||
|
<string name="profiles_builder_skills_search_results">Search Results:</string>
|
||||||
|
<string name="profiles_builder_mcp_title">Configured MCP Servers</string>
|
||||||
|
<string name="profiles_builder_mcp_add">Add Server</string>
|
||||||
|
<string name="profiles_builder_mcp_name">Server Name</string>
|
||||||
|
<string name="profiles_builder_mcp_transport">Transport Protocol</string>
|
||||||
|
<string name="profiles_builder_mcp_dialog_title">Add MCP Server</string>
|
||||||
|
<string name="profiles_builder_mcp_transport_stdio">Stdio Command</string>
|
||||||
|
<string name="profiles_builder_mcp_transport_sse">SSE Server URL</string>
|
||||||
|
<string name="profiles_builder_mcp_command">Command</string>
|
||||||
|
<string name="profiles_builder_mcp_args">Arguments (comma-separated)</string>
|
||||||
|
<string name="profiles_builder_mcp_sse_url">SSE URL</string>
|
||||||
|
<string name="profiles_builder_mcp_env">Environment Variables (KEY=VALUE, one per line)</string>
|
||||||
|
<string name="profiles_builder_review_title">Review Profile Details</string>
|
||||||
|
<string name="profiles_builder_review_name">Profile Name</string>
|
||||||
|
<string name="profiles_builder_review_description">Description</string>
|
||||||
|
<string name="profiles_builder_review_provider">Provider: %1$s</string>
|
||||||
|
<string name="profiles_builder_review_model">Model: %1$s</string>
|
||||||
|
<string name="profiles_builder_review_model_settings">Model Settings</string>
|
||||||
|
<string name="profiles_builder_review_system_defaults">Using system defaults</string>
|
||||||
|
<string name="profiles_builder_review_skills_title">Skills Configuration</string>
|
||||||
|
<string name="profiles_builder_review_skills_default">Full Default Bundle</string>
|
||||||
|
<string name="profiles_builder_review_skills_local">%1$d local skills selected</string>
|
||||||
|
<string name="profiles_builder_review_skills_hub">%1$d hub skills added</string>
|
||||||
|
<string name="profiles_builder_review_mcp_title">MCP Servers</string>
|
||||||
|
<string name="profiles_builder_review_mcp_none">No MCP servers configured</string>
|
||||||
|
<string name="profiles_builder_mcp_none">No MCP servers configured yet</string>
|
||||||
|
<string name="profiles_builder_action_add">Add</string>
|
||||||
|
|
||||||
<!-- Plugins Screen -->
|
<!-- Plugins Screen -->
|
||||||
<string name="plugins_empty_title">No plugins found</string>
|
<string name="plugins_empty_title">No plugins found</string>
|
||||||
|
|||||||
@@ -1005,7 +1005,8 @@ class E2eIntegrationTest {
|
|||||||
|
|
||||||
assertEquals(
|
assertEquals(
|
||||||
50.0,
|
50.0,
|
||||||
viewModel.uiState.value.stats?.cpu_percent,
|
viewModel.uiState.value.stats
|
||||||
|
?.cpu_percent,
|
||||||
)
|
)
|
||||||
|
|
||||||
viewModel.triggerBackup()
|
viewModel.triggerBackup()
|
||||||
|
|||||||
@@ -574,16 +574,26 @@ class ConnectViewModelTest {
|
|||||||
fun testMultipleProfiles_loadedOnInit() {
|
fun testMultipleProfiles_loadedOnInit() {
|
||||||
val profiles =
|
val profiles =
|
||||||
listOf(
|
listOf(
|
||||||
com.m57.hermescontrol.data.model.ConnectionProfile("a", "Alpha", "10.0.0.1", 9119),
|
com.m57.hermescontrol.data.model
|
||||||
com.m57.hermescontrol.data.model.ConnectionProfile("b", "Beta", "10.0.0.2", 9220),
|
.ConnectionProfile("a", "Alpha", "10.0.0.1", 9119),
|
||||||
|
com.m57.hermescontrol.data.model
|
||||||
|
.ConnectionProfile("b", "Beta", "10.0.0.2", 9220),
|
||||||
)
|
)
|
||||||
every { AuthManager.getConnectionProfiles() } returns profiles
|
every { AuthManager.getConnectionProfiles() } returns profiles
|
||||||
every { AuthManager.getSelectedProfileId() } returns null
|
every { AuthManager.getSelectedProfileId() } returns null
|
||||||
|
|
||||||
val viewModel = ConnectViewModel(mockApp)
|
val viewModel = ConnectViewModel(mockApp)
|
||||||
assertEquals(2, viewModel.uiState.value.profiles.size)
|
assertEquals(2, viewModel.uiState.value.profiles.size)
|
||||||
assertEquals("Alpha", viewModel.uiState.value.profiles[0].name)
|
assertEquals(
|
||||||
assertEquals("Beta", viewModel.uiState.value.profiles[1].name)
|
"Alpha",
|
||||||
|
viewModel.uiState.value.profiles[0]
|
||||||
|
.name,
|
||||||
|
)
|
||||||
|
assertEquals(
|
||||||
|
"Beta",
|
||||||
|
viewModel.uiState.value.profiles[1]
|
||||||
|
.name,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
@@ -67,7 +67,10 @@ class ModelViewModelTest {
|
|||||||
@Test
|
@Test
|
||||||
fun `init with empty pinned models`() {
|
fun `init with empty pinned models`() {
|
||||||
val viewModel = createViewModel()
|
val viewModel = createViewModel()
|
||||||
assertTrue(viewModel.uiState.value.pinnedModels.isEmpty())
|
assertTrue(
|
||||||
|
viewModel.uiState.value.pinnedModels
|
||||||
|
.isEmpty(),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
@@ -178,7 +181,10 @@ class ModelViewModelTest {
|
|||||||
|
|
||||||
viewModel.unpinModel("openai", "gpt-4")
|
viewModel.unpinModel("openai", "gpt-4")
|
||||||
testDispatcher.scheduler.advanceUntilIdle()
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
assertTrue(viewModel.uiState.value.pinnedModels.isEmpty())
|
assertTrue(
|
||||||
|
viewModel.uiState.value.pinnedModels
|
||||||
|
.isEmpty(),
|
||||||
|
)
|
||||||
|
|
||||||
// Re-pin same model after unpin
|
// Re-pin same model after unpin
|
||||||
viewModel.pinModel("openai", "gpt-4")
|
viewModel.pinModel("openai", "gpt-4")
|
||||||
|
|||||||
Reference in new issue
Block a user