refactor: migrate Gson serialization to kotlinx.serialization (#475)
* refactor: migrate Gson serialization to kotlinx.serialization * fix(dto): resolve serializing Any? and Map<String, Any?> with JsonElement * test(refactor): fix unit test issues under kotlinx.serialization and update nullable DTO defaults * fix(chat): support flexible numeric and string decoding for message timestamps * fix(chat): sequentialize cache loading to avoid overwriting network state
This commit is contained in:
64 files changed
+863
-577
No files matched your search
@@ -4,6 +4,9 @@ import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import androidx.security.crypto.EncryptedSharedPreferences
|
||||
import androidx.security.crypto.MasterKeys
|
||||
import com.m57.hermescontrol.data.model.ConnectionProfile
|
||||
import com.m57.hermescontrol.data.model.PinnedModel
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import com.m57.hermescontrol.theme.BottomNavDisplayMode
|
||||
import com.m57.hermescontrol.theme.ThemePreference
|
||||
import com.m57.hermescontrol.theme.ThemePreset
|
||||
@@ -15,6 +18,8 @@ import kotlinx.coroutines.flow.MutableStateFlow
|
||||
import kotlinx.coroutines.flow.StateFlow
|
||||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
/**
|
||||
* Singleton that manages encrypted storage of the Hermes dashboard token
|
||||
@@ -69,8 +74,6 @@ object AuthManager {
|
||||
private val _tokenFlow = MutableStateFlow<String?>(null)
|
||||
val tokenFlow: StateFlow<String?> = _tokenFlow.asStateFlow()
|
||||
|
||||
private val gson = com.m57.hermescontrol.data.remote.OkHttpProvider.gson
|
||||
|
||||
/**
|
||||
* Initialise the encrypted preferences.
|
||||
* Call this once from Application.onCreate() or MainActivity.onCreate().
|
||||
@@ -160,14 +163,10 @@ object AuthManager {
|
||||
|
||||
// ── Connection Profiles ──────────────────────────────────────────────
|
||||
|
||||
fun getConnectionProfiles(): List<com.m57.hermescontrol.data.model.ConnectionProfile> {
|
||||
fun getConnectionProfiles(): List<ConnectionProfile> {
|
||||
val json = requirePrefs().getString(KEY_CONNECTION_PROFILES, null) ?: return emptyList()
|
||||
return try {
|
||||
val type =
|
||||
object : com.google.gson.reflect.TypeToken<
|
||||
List<com.m57.hermescontrol.data.model.ConnectionProfile>,
|
||||
>() {}.type
|
||||
gson.fromJson(json, type) ?: emptyList()
|
||||
OkHttpProvider.json.decodeFromString<List<ConnectionProfile>>(json)
|
||||
} catch (e: Exception) {
|
||||
if (com.m57.hermescontrol.BuildConfig.DEBUG) {
|
||||
android.util.Log.w("AuthManager", "Failed to parse connection profiles", e)
|
||||
@@ -176,21 +175,17 @@ object AuthManager {
|
||||
}
|
||||
}
|
||||
|
||||
fun saveConnectionProfiles(profiles: List<com.m57.hermescontrol.data.model.ConnectionProfile>) {
|
||||
val json = gson.toJson(profiles)
|
||||
fun saveConnectionProfiles(profiles: List<ConnectionProfile>) {
|
||||
val json = OkHttpProvider.json.encodeToString(profiles)
|
||||
requirePrefs().edit().putString(KEY_CONNECTION_PROFILES, json).apply()
|
||||
}
|
||||
|
||||
// ── Pinned Models ────────────────────────────────────────────────────
|
||||
|
||||
fun getPinnedModels(): List<com.m57.hermescontrol.data.model.PinnedModel> {
|
||||
fun getPinnedModels(): List<PinnedModel> {
|
||||
val json = requirePrefs().getString(KEY_PINNED_MODELS, null) ?: return emptyList()
|
||||
return try {
|
||||
val type =
|
||||
object : com.google.gson.reflect.TypeToken<
|
||||
List<com.m57.hermescontrol.data.model.PinnedModel>,
|
||||
>() {}.type
|
||||
gson.fromJson(json, type) ?: emptyList()
|
||||
OkHttpProvider.json.decodeFromString<List<PinnedModel>>(json)
|
||||
} catch (e: Exception) {
|
||||
if (com.m57.hermescontrol.BuildConfig.DEBUG) {
|
||||
android.util.Log.w("AuthManager", "Failed to parse pinned models", e)
|
||||
@@ -200,7 +195,9 @@ object AuthManager {
|
||||
}
|
||||
|
||||
fun savePinnedModels(pinned: List<com.m57.hermescontrol.data.model.PinnedModel>) {
|
||||
val json = gson.toJson(pinned)
|
||||
val json =
|
||||
com.m57.hermescontrol.data.remote.OkHttpProvider.json
|
||||
.encodeToString(pinned)
|
||||
requirePrefs().edit().putString(KEY_PINNED_MODELS, json).apply()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,68 +1,73 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Serializable
|
||||
data class AchievementsResponse(
|
||||
val achievements: List<Achievement>? = emptyList(),
|
||||
@SerializedName("unlocked_count") val unlockedCount: Int = 0,
|
||||
@SerializedName("discovered_count") val discoveredCount: Int = 0,
|
||||
@SerializedName("secret_count") val secretCount: Int = 0,
|
||||
@SerializedName("total_count") val totalCount: Int = 0,
|
||||
@SerializedName("is_stale") val isStale: Boolean = false,
|
||||
@SerializedName("generated_at") val generatedAt: Long? = null,
|
||||
@SerializedName("scan_meta") val scanMeta: ScanMeta? = null,
|
||||
@SerialName("unlocked_count") val unlockedCount: Int = 0,
|
||||
@SerialName("discovered_count") val discoveredCount: Int = 0,
|
||||
@SerialName("secret_count") val secretCount: Int = 0,
|
||||
@SerialName("total_count") val totalCount: Int = 0,
|
||||
@SerialName("is_stale") val isStale: Boolean = false,
|
||||
@SerialName("generated_at") val generatedAt: Long? = null,
|
||||
@SerialName("scan_meta") val scanMeta: ScanMeta? = null,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ScanMeta(
|
||||
val status: ScanStatus? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ScanStatus(
|
||||
val state: String = "idle",
|
||||
@SerializedName("started_at") val startedAt: Long? = null,
|
||||
@SerializedName("finished_at") val finishedAt: Long? = null,
|
||||
@SerializedName("last_error") val lastError: String? = null,
|
||||
@SerializedName("last_duration_ms") val lastDurationMs: Long? = null,
|
||||
@SerializedName("run_count") val runCount: Int = 0,
|
||||
@SerialName("started_at") val startedAt: Long? = null,
|
||||
@SerialName("finished_at") val finishedAt: Long? = null,
|
||||
@SerialName("last_error") val lastError: String? = null,
|
||||
@SerialName("last_duration_ms") val lastDurationMs: Long? = null,
|
||||
@SerialName("run_count") val runCount: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class RecentUnlock(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val category: String?,
|
||||
val kind: String?,
|
||||
val icon: String?,
|
||||
val description: String? = null,
|
||||
val category: String? = null,
|
||||
val kind: String? = null,
|
||||
val icon: String? = null,
|
||||
val unlocked: Boolean,
|
||||
val discovered: Boolean,
|
||||
val state: String?,
|
||||
val tier: String?,
|
||||
val progress: Double?,
|
||||
@SerializedName("next_tier") val nextTier: String?,
|
||||
@SerializedName("next_threshold") val nextThreshold: Double?,
|
||||
@SerializedName("progress_pct") val progressPct: Double?,
|
||||
@SerializedName("unlocked_at") val unlockedAt: Long?,
|
||||
val secret: Boolean?,
|
||||
val criteria: String?,
|
||||
val state: String? = null,
|
||||
val tier: String? = null,
|
||||
val progress: Double? = null,
|
||||
@SerialName("next_tier") val nextTier: String? = null,
|
||||
@SerialName("next_threshold") val nextThreshold: Double? = null,
|
||||
@SerialName("progress_pct") val progressPct: Double? = null,
|
||||
@SerialName("unlocked_at") val unlockedAt: Long? = null,
|
||||
val secret: Boolean? = null,
|
||||
val criteria: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class Achievement(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val category: String?,
|
||||
val kind: String?,
|
||||
val icon: String?,
|
||||
val description: String? = null,
|
||||
val category: String? = null,
|
||||
val kind: String? = null,
|
||||
val icon: String? = null,
|
||||
val unlocked: Boolean,
|
||||
val discovered: Boolean,
|
||||
val state: String?,
|
||||
val tier: String?,
|
||||
val progress: Double?,
|
||||
@SerializedName("next_tier") val nextTier: String?,
|
||||
@SerializedName("next_threshold") val nextThreshold: Double?,
|
||||
@SerializedName("progress_pct") val progressPct: Double?,
|
||||
@SerializedName("unlocked_at") val unlockedAt: Long?,
|
||||
val secret: Boolean?,
|
||||
val criteria: String?,
|
||||
val state: String? = null,
|
||||
val tier: String? = null,
|
||||
val progress: Double? = null,
|
||||
@SerialName("next_tier") val nextTier: String? = null,
|
||||
@SerialName("next_threshold") val nextThreshold: Double? = null,
|
||||
@SerialName("progress_pct") val progressPct: Double? = null,
|
||||
@SerialName("unlocked_at") val unlockedAt: Long? = null,
|
||||
val secret: Boolean? = null,
|
||||
val criteria: String? = null,
|
||||
)
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ActionResponse(
|
||||
val archive: String? = null,
|
||||
val name: String? = null,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ActionStatusResponse(
|
||||
val exit_code: Int? = null,
|
||||
val lines: List<String>? = null,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Represents a file attached to a chat message.
|
||||
@@ -9,6 +10,7 @@ package com.m57.hermescontrol.data.model
|
||||
* @param size File size in bytes
|
||||
* @param localPath Optional local file system path for direct access
|
||||
*/
|
||||
@Serializable
|
||||
data class Attachment(
|
||||
val uri: String,
|
||||
val name: String,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CheckpointsResponse(
|
||||
val sessions: List<CheckpointSession>? = null,
|
||||
val total_bytes: Long? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CheckpointSession(
|
||||
val session: String? = null,
|
||||
val files: Int? = null,
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
|
||||
@Serializable
|
||||
data class RawConfigResponse(
|
||||
val path: String?,
|
||||
val yaml: String?,
|
||||
val path: String? = null,
|
||||
val yaml: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateRawConfigRequest(
|
||||
val yaml_text: String,
|
||||
val profile: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ConfigSchemaResponse(
|
||||
val fields: Map<String, SchemaField>,
|
||||
val category_order: List<String>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SchemaField(
|
||||
val type: String,
|
||||
val description: String? = null,
|
||||
@@ -24,6 +28,7 @@ data class SchemaField(
|
||||
val options: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ConfigUpdateRequest(
|
||||
val config: Map<String, JsonElement>,
|
||||
val profile: String? = null,
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import java.util.UUID
|
||||
|
||||
@Serializable
|
||||
data class ConnectionProfile(
|
||||
val id: String = UUID.randomUUID().toString(),
|
||||
val name: String,
|
||||
|
||||
@@ -1,14 +1,18 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CredentialPoolResponse(
|
||||
val providers: List<CredentialPoolProvider>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CredentialPoolProvider(
|
||||
val provider: String? = null,
|
||||
val entries: List<CredentialPoolEntry>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CredentialPoolEntry(
|
||||
val index: Int? = null,
|
||||
val label: String? = null,
|
||||
|
||||
@@ -1,17 +1,23 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
@Serializable
|
||||
data class CronJobRepeat(
|
||||
val times: Int? = null,
|
||||
val completed: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CronJob(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val schedule: Any?,
|
||||
val state: String?,
|
||||
val last_run_status: String?,
|
||||
val next_run: String?,
|
||||
val schedule: JsonElement? = null,
|
||||
val state: String? = null,
|
||||
val last_run_status: String? = null,
|
||||
val next_run: String? = null,
|
||||
val schedule_display: String? = null,
|
||||
val last_status: String? = null,
|
||||
val next_run_at: String? = null,
|
||||
@@ -36,8 +42,8 @@ data class CronJob(
|
||||
val scheduleText: String
|
||||
get() =
|
||||
when (schedule) {
|
||||
is String -> schedule
|
||||
is Map<*, *> -> (schedule["display"] as? String) ?: schedule_display ?: ""
|
||||
is JsonPrimitive -> schedule.content
|
||||
is JsonObject -> (schedule["display"] as? JsonPrimitive)?.content ?: schedule_display ?: ""
|
||||
else -> schedule_display ?: ""
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
|
||||
@Serializable
|
||||
data class CreateCronJobRequest(
|
||||
val name: String = "",
|
||||
val schedule: String,
|
||||
@@ -17,6 +20,7 @@ data class CreateCronJobRequest(
|
||||
val repeat: Int? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateCronJobRequest(
|
||||
val updates: Map<String, Any?>,
|
||||
val updates: Map<String, JsonElement>,
|
||||
)
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class CuratorResponse(
|
||||
val enabled: Boolean? = null,
|
||||
val paused: Boolean? = null,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DebugShareResponse(
|
||||
val ok: Boolean? = null,
|
||||
val urls: Map<String, String>? = null,
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class DoctorResponse(
|
||||
val ok: Boolean,
|
||||
val pid: Int?,
|
||||
val name: String?,
|
||||
val pid: Int? = null,
|
||||
val name: String? = null,
|
||||
)
|
||||
@@ -1,32 +1,37 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Serializable
|
||||
data class EnvVarConfig(
|
||||
@SerializedName("is_set") val isSet: Boolean,
|
||||
@SerializedName("redacted_value") val redactedValue: String?,
|
||||
val description: String?,
|
||||
val url: String?,
|
||||
val category: String?,
|
||||
@SerializedName("is_password") val isPassword: Boolean,
|
||||
@SerialName("is_set") val isSet: Boolean,
|
||||
@SerialName("redacted_value") val redactedValue: String? = null,
|
||||
val description: String? = null,
|
||||
val url: String? = null,
|
||||
val category: String? = null,
|
||||
@SerialName("is_password") val isPassword: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EnvVarRevealRequest(
|
||||
val key: String,
|
||||
val profile: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EnvVarRevealResponse(
|
||||
val key: String,
|
||||
val value: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EnvVarUpdate(
|
||||
val key: String,
|
||||
val value: String,
|
||||
val profile: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EnvVarDeleteRequest(
|
||||
val key: String,
|
||||
val profile: String? = null,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class HookResponse(
|
||||
val hooks: List<HookEntry>? = null,
|
||||
val valid_events: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HookEntry(
|
||||
val event: String? = null,
|
||||
val matcher: String? = null,
|
||||
|
||||
@@ -1,37 +1,43 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Serializable
|
||||
data class KanbanBoard(
|
||||
@SerializedName("slug") val id: String,
|
||||
@SerialName("slug") val id: String,
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val description: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KanbanTask(
|
||||
val id: String,
|
||||
val title: String,
|
||||
@SerializedName("body") val description: String?,
|
||||
@SerialName("body") val description: String? = null,
|
||||
val status: String,
|
||||
@SerializedName("assignee") val assignedTo: String?,
|
||||
@SerialName("assignee") val assignedTo: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreateTaskBody(
|
||||
val title: String,
|
||||
val body: String?,
|
||||
val body: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KanbanColumn(
|
||||
val name: String,
|
||||
val tasks: List<KanbanTask>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KanbanBoardResponse(
|
||||
val columns: List<KanbanColumn>,
|
||||
val assignees: List<String>?,
|
||||
val tenants: List<String>?,
|
||||
val assignees: List<String>? = null,
|
||||
val tenants: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class KanbanBoardsResponse(
|
||||
val boards: List<KanbanBoard>,
|
||||
val current: String? = null,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class LogResponse(
|
||||
val lines: List<String>? = null,
|
||||
val logs: List<String>? = null,
|
||||
|
||||
@@ -1,25 +1,30 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class McpServersResponse(
|
||||
val servers: List<McpServer>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class McpServer(
|
||||
val name: String,
|
||||
val transport: String?,
|
||||
val url: String?,
|
||||
val command: String?,
|
||||
val args: List<String>?,
|
||||
val env: Map<String, String>?,
|
||||
val transport: String? = null,
|
||||
val url: String? = null,
|
||||
val command: String? = null,
|
||||
val args: List<String>? = null,
|
||||
val env: Map<String, String>? = null,
|
||||
val enabled: Boolean,
|
||||
val status: String? = null,
|
||||
val error: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class McpServerToggleRequest(
|
||||
val enabled: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AddMcpServerRequest(
|
||||
val name: String,
|
||||
val url: String? = null,
|
||||
@@ -28,10 +33,12 @@ data class AddMcpServerRequest(
|
||||
val env: Map<String, String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class McpCatalogResponse(
|
||||
val entries: List<McpCatalogEntry>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class McpCatalogEntry(
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
@@ -42,6 +49,7 @@ data class McpCatalogEntry(
|
||||
val env: List<McpCatalogEnvVar>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class McpCatalogEnvVar(
|
||||
val key: String,
|
||||
val label: String? = null,
|
||||
@@ -49,6 +57,7 @@ data class McpCatalogEnvVar(
|
||||
val required: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class McpCatalogInstallRequest(
|
||||
val name: String,
|
||||
val env: Map<String, String>? = null,
|
||||
|
||||
@@ -1,10 +1,13 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class MemoryResponse(
|
||||
val active: String? = null,
|
||||
val builtin_files: BuiltinFileSizes? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BuiltinFileSizes(
|
||||
val memory: Long? = null,
|
||||
val user: Long? = null,
|
||||
|
||||
@@ -1,57 +1,63 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Serializable
|
||||
data class MessagingPlatform(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
@SerializedName("docs_url") val docsUrl: String? = null,
|
||||
@SerialName("docs_url") val docsUrl: String? = null,
|
||||
val enabled: Boolean,
|
||||
val configured: Boolean,
|
||||
@SerializedName("gateway_running") val gatewayRunning: Boolean = false,
|
||||
@SerialName("gateway_running") val gatewayRunning: Boolean = false,
|
||||
val state: String = "disabled",
|
||||
@SerializedName("error_code") val errorCode: String? = null,
|
||||
@SerializedName("error_message") val errorMessage: String? = null,
|
||||
@SerializedName("env_vars") val envVars: List<EnvVarField>? = null,
|
||||
@SerializedName("updated_at") val updatedAt: String? = null,
|
||||
@SerializedName("home_channel") val homeChannel: HomeChannelInfo? = null,
|
||||
@SerialName("error_code") val errorCode: String? = null,
|
||||
@SerialName("error_message") val errorMessage: String? = null,
|
||||
@SerialName("env_vars") val envVars: List<EnvVarField>? = null,
|
||||
@SerialName("updated_at") val updatedAt: String? = null,
|
||||
@SerialName("home_channel") val homeChannel: HomeChannelInfo? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HomeChannelInfo(
|
||||
val platform: String,
|
||||
@SerializedName("chat_id") val chatId: String,
|
||||
@SerialName("chat_id") val chatId: String,
|
||||
val name: String,
|
||||
@SerializedName("thread_id") val threadId: String? = null,
|
||||
@SerialName("thread_id") val threadId: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class EnvVarField(
|
||||
val key: String,
|
||||
val required: Boolean,
|
||||
@SerializedName("is_set") val isSet: Boolean,
|
||||
@SerializedName("redacted_value") val redactedValue: String? = null,
|
||||
@SerialName("is_set") val isSet: Boolean,
|
||||
@SerialName("redacted_value") val redactedValue: String? = null,
|
||||
val description: String? = null,
|
||||
val prompt: String? = null,
|
||||
val help: String? = null,
|
||||
@SerializedName("is_password") val isPassword: Boolean,
|
||||
@SerialName("is_password") val isPassword: Boolean,
|
||||
val advanced: Boolean = false,
|
||||
val url: String? = null,
|
||||
@SerializedName("expires_at") val expiresAt: String? = null,
|
||||
@SerialName("expires_at") val expiresAt: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MessagingPlatformResponse(
|
||||
@SerializedName("env_path") val envPath: String,
|
||||
@SerializedName("gateway_start_command") val gatewayStartCommand: String,
|
||||
@SerialName("env_path") val envPath: String,
|
||||
@SerialName("gateway_start_command") val gatewayStartCommand: String,
|
||||
val platforms: List<MessagingPlatform>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MessagingPlatformUpdate(
|
||||
val enabled: Boolean? = null,
|
||||
val env: Map<String, String> = emptyMap(),
|
||||
@SerializedName("clear_env") val clearEnv: List<String> = emptyList(),
|
||||
@SerialName("clear_env") val clearEnv: List<String> = emptyList(),
|
||||
val profile: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MessagingPlatformTestResult(
|
||||
val ok: Boolean,
|
||||
val state: String,
|
||||
@@ -60,37 +66,42 @@ data class MessagingPlatformTestResult(
|
||||
|
||||
// ── Telegram onboarding ─────────────────────────────────────────────────
|
||||
|
||||
@Serializable
|
||||
data class TelegramOnboardingStartRequest(
|
||||
@SerializedName("bot_name") val botName: String = "Hermes Agent",
|
||||
@SerialName("bot_name") val botName: String = "Hermes Agent",
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TelegramOnboardingStartResponse(
|
||||
@SerializedName("pairing_id") val pairingId: String,
|
||||
@SerializedName("suggested_username") val suggestedUsername: String,
|
||||
@SerializedName("deep_link") val deepLink: String,
|
||||
@SerializedName("qr_payload") val qrPayload: String,
|
||||
@SerializedName("expires_at") val expiresAt: String,
|
||||
@SerialName("pairing_id") val pairingId: String,
|
||||
@SerialName("suggested_username") val suggestedUsername: String,
|
||||
@SerialName("deep_link") val deepLink: String,
|
||||
@SerialName("qr_payload") val qrPayload: String,
|
||||
@SerialName("expires_at") val expiresAt: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TelegramOnboardingStatusResponse(
|
||||
val status: String,
|
||||
@SerializedName("bot_username") val botUsername: String? = null,
|
||||
@SerializedName("owner_user_id") val ownerUserId: String? = null,
|
||||
@SerializedName("expires_at") val expiresAt: String? = null,
|
||||
@SerialName("bot_username") val botUsername: String? = null,
|
||||
@SerialName("owner_user_id") val ownerUserId: String? = null,
|
||||
@SerialName("expires_at") val expiresAt: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TelegramOnboardingApplyRequest(
|
||||
@SerializedName("allowed_user_ids") val allowedUserIds: List<String>,
|
||||
@SerialName("allowed_user_ids") val allowedUserIds: List<String>,
|
||||
val profile: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class TelegramOnboardingApplyResponse(
|
||||
val ok: Boolean,
|
||||
val platform: String,
|
||||
@SerializedName("bot_username") val botUsername: String? = null,
|
||||
@SerializedName("needs_restart") val needsRestart: Boolean,
|
||||
@SerializedName("restart_started") val restartStarted: Boolean? = null,
|
||||
@SerializedName("restart_action") val restartAction: String? = null,
|
||||
@SerializedName("restart_pid") val restartPid: Int? = null,
|
||||
@SerializedName("restart_error") val restartError: String? = null,
|
||||
@SerialName("bot_username") val botUsername: String? = null,
|
||||
@SerialName("needs_restart") val needsRestart: Boolean,
|
||||
@SerialName("restart_started") val restartStarted: Boolean? = null,
|
||||
@SerialName("restart_action") val restartAction: String? = null,
|
||||
@SerialName("restart_pid") val restartPid: Int? = null,
|
||||
@SerialName("restart_error") val restartError: String? = null,
|
||||
)
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class AuxiliaryTaskAssignment(
|
||||
val task: String,
|
||||
val provider: String,
|
||||
@@ -7,21 +9,25 @@ data class AuxiliaryTaskAssignment(
|
||||
val base_url: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AuxiliaryModelsResponse(
|
||||
val tasks: List<AuxiliaryTaskAssignment>,
|
||||
val main: MainModelAssignment,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MainModelAssignment(
|
||||
val provider: String,
|
||||
val model: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MoaModelSlot(
|
||||
val provider: String,
|
||||
val model: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MoaConfigPreset(
|
||||
val reference_models: List<MoaModelSlot>,
|
||||
val aggregator: MoaModelSlot,
|
||||
@@ -31,6 +37,7 @@ data class MoaConfigPreset(
|
||||
val enabled: Boolean = true,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MoaConfigResponse(
|
||||
val default_preset: String = "",
|
||||
val active_preset: String = "",
|
||||
@@ -43,6 +50,7 @@ data class MoaConfigResponse(
|
||||
val enabled: Boolean = true,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ModelAssignmentRequest(
|
||||
val confirm_expensive_model: Boolean = false,
|
||||
val scope: String,
|
||||
@@ -52,6 +60,7 @@ data class ModelAssignmentRequest(
|
||||
val task: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ModelAssignmentResponse(
|
||||
val confirm_message: String? = null,
|
||||
val confirm_required: Boolean? = null,
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ModelOptionsResponse(
|
||||
val providers: List<ModelProvider>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ModelProvider(
|
||||
val slug: String,
|
||||
val name: String,
|
||||
val is_current: Boolean?,
|
||||
val is_user_defined: Boolean?,
|
||||
val models: List<String>?,
|
||||
val total_models: Int?,
|
||||
val source: String?,
|
||||
val authenticated: Boolean?,
|
||||
val auth_type: String?,
|
||||
val warning: String?,
|
||||
val is_current: Boolean? = null,
|
||||
val is_user_defined: Boolean? = null,
|
||||
val models: List<String>? = null,
|
||||
val total_models: Int? = null,
|
||||
val source: String? = null,
|
||||
val authenticated: Boolean? = null,
|
||||
val auth_type: String? = null,
|
||||
val warning: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PinnedModel(
|
||||
val providerSlug: String,
|
||||
val modelName: String,
|
||||
|
||||
@@ -1,24 +1,29 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PairingResponse(
|
||||
val pending: List<PairingItem>,
|
||||
val approved: List<PairingItem>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PairingItem(
|
||||
val platform: String,
|
||||
val user_id: String?,
|
||||
val username: String?,
|
||||
val display_name: String?,
|
||||
val code: String?,
|
||||
val created_at: String?,
|
||||
val user_id: String? = null,
|
||||
val username: String? = null,
|
||||
val display_name: String? = null,
|
||||
val code: String? = null,
|
||||
val created_at: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PairingApproveRequest(
|
||||
val platform: String,
|
||||
val code: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PairingRevokeRequest(
|
||||
val platform: String,
|
||||
val user_id: String,
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Serializable
|
||||
data class PluginInfo(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val version: String?,
|
||||
val source: String?,
|
||||
@SerializedName("runtime_status") val runtimeStatus: String?,
|
||||
@SerializedName("has_dashboard_manifest") val hasDashboardManifest: Boolean = false,
|
||||
@SerializedName("dashboard_manifest") val dashboardManifest: PluginManifestData? = null,
|
||||
@SerializedName("can_remove") val canRemove: Boolean = false,
|
||||
@SerializedName("can_update_git") val canUpdateGit: Boolean = false,
|
||||
@SerializedName("auth_required") val authRequired: Boolean = false,
|
||||
@SerializedName("auth_command") val authCommand: String? = null,
|
||||
@SerializedName("user_hidden") val userHidden: Boolean = false,
|
||||
val description: String? = null,
|
||||
val version: String? = null,
|
||||
val source: String? = null,
|
||||
@SerialName("runtime_status") val runtimeStatus: String? = null,
|
||||
@SerialName("has_dashboard_manifest") val hasDashboardManifest: Boolean = false,
|
||||
@SerialName("dashboard_manifest") val dashboardManifest: PluginManifestData? = null,
|
||||
@SerialName("can_remove") val canRemove: Boolean = false,
|
||||
@SerialName("can_update_git") val canUpdateGit: Boolean = false,
|
||||
@SerialName("auth_required") val authRequired: Boolean = false,
|
||||
@SerialName("auth_command") val authCommand: String? = null,
|
||||
@SerialName("user_hidden") val userHidden: Boolean = false,
|
||||
) {
|
||||
val enabled: Boolean
|
||||
get() = runtimeStatus.equals("enabled", ignoreCase = true)
|
||||
@@ -25,6 +26,7 @@ data class PluginInfo(
|
||||
runtimeStatus.equals("disabled", ignoreCase = true)
|
||||
}
|
||||
|
||||
@Serializable
|
||||
data class PluginManifestData(
|
||||
val name: String? = null,
|
||||
val label: String? = null,
|
||||
@@ -35,10 +37,11 @@ data class PluginManifestData(
|
||||
val slots: List<String>? = null,
|
||||
val entry: String? = null,
|
||||
val css: String? = null,
|
||||
@SerializedName("has_api") val hasApi: Boolean = false,
|
||||
@SerialName("has_api") val hasApi: Boolean = false,
|
||||
val source: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PluginTabInfo(
|
||||
val path: String? = null,
|
||||
val position: String? = null,
|
||||
@@ -46,6 +49,7 @@ data class PluginTabInfo(
|
||||
val hidden: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DashboardPluginMeta(
|
||||
val name: String? = null,
|
||||
val label: String? = null,
|
||||
@@ -53,31 +57,36 @@ data class DashboardPluginMeta(
|
||||
val tab: PluginTabInfo? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PluginsHubProviders(
|
||||
@SerializedName("memory_provider") val memoryProvider: String? = null,
|
||||
@SerializedName("memory_options") val memoryOptions: List<ProviderOption> = emptyList(),
|
||||
@SerializedName("context_engine") val contextEngine: String? = null,
|
||||
@SerializedName("context_options") val contextOptions: List<ProviderOption> = emptyList(),
|
||||
@SerialName("memory_provider") val memoryProvider: String? = null,
|
||||
@SerialName("memory_options") val memoryOptions: List<ProviderOption> = emptyList(),
|
||||
@SerialName("context_engine") val contextEngine: String? = null,
|
||||
@SerialName("context_options") val contextOptions: List<ProviderOption> = emptyList(),
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProviderOption(
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PluginsHubResponse(
|
||||
val plugins: List<PluginInfo>,
|
||||
@SerializedName("orphan_dashboard_plugins") val orphanDashboardPlugins: List<DashboardPluginMeta> = emptyList(),
|
||||
@SerialName("orphan_dashboard_plugins") val orphanDashboardPlugins: List<DashboardPluginMeta> = emptyList(),
|
||||
val providers: PluginsHubProviders? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class AgentPluginInstallBody(
|
||||
val identifier: String,
|
||||
val force: Boolean = false,
|
||||
val enable: Boolean = true,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PluginProvidersPutRequest(
|
||||
@SerializedName("memory_provider") val memoryProvider: String? = null,
|
||||
@SerializedName("context_engine") val contextEngine: String? = null,
|
||||
@SerialName("memory_provider") val memoryProvider: String? = null,
|
||||
@SerialName("context_engine") val contextEngine: String? = null,
|
||||
)
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class PortalResponse(
|
||||
val logged_in: Boolean? = null,
|
||||
val portal_url: String? = null,
|
||||
@@ -9,6 +11,7 @@ data class PortalResponse(
|
||||
val features: List<PortalFeature>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PortalFeature(
|
||||
val label: String? = null,
|
||||
val state: String? = null,
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ProfilesResponse(
|
||||
val profiles: List<ProfileInfo>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProfileInfo(
|
||||
val name: String,
|
||||
val path: String? = null,
|
||||
@@ -17,46 +20,55 @@ data class ProfileInfo(
|
||||
val description_auto: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ActiveProfileResponse(
|
||||
val active: String,
|
||||
val current: String?,
|
||||
val current: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SetActiveProfileRequest(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProfileSoulResponse(
|
||||
val content: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateProfileSoulRequest(
|
||||
val content: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateProfileModelRequest(
|
||||
val provider: String,
|
||||
val model: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CloneProfileRequest(
|
||||
val name: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class UpdateProfileDescriptionRequest(
|
||||
val description: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
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>?,
|
||||
val description: String? = null,
|
||||
val provider: String? = null,
|
||||
val model: String? = null,
|
||||
val mcp_servers: List<McpServerConfigInput>? = null,
|
||||
val keep_skills: Boolean? = null,
|
||||
val hub_skills: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class McpServerConfigInput(
|
||||
val name: String,
|
||||
val transport: String,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SaveSkillContentRequest(
|
||||
val name: String,
|
||||
val content: String,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SessionListResponse(
|
||||
val sessions: List<SessionInfo>,
|
||||
val total: Int = 0,
|
||||
@@ -7,35 +9,41 @@ data class SessionListResponse(
|
||||
val offset: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SessionInfo(
|
||||
val id: String,
|
||||
val title: String?,
|
||||
val created_at: String?,
|
||||
val message_count: Int?,
|
||||
val status: String?,
|
||||
val title: String? = null,
|
||||
val created_at: String? = null,
|
||||
val message_count: Int? = null,
|
||||
val status: String? = null,
|
||||
val preview: String? = null,
|
||||
val started_at: Double? = null,
|
||||
val source: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SessionStatsResponse(
|
||||
val total: Int = 0,
|
||||
val active: Int = 0,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SessionRenameRequest(
|
||||
val title: String,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class BulkDeleteRequest(
|
||||
val ids: List<String>,
|
||||
val delete_all: Boolean = false,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PruneRequest(
|
||||
val days: Int,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SessionPromptResponse(
|
||||
val prompt: String? = null,
|
||||
val id: String? = null,
|
||||
|
||||
@@ -1,12 +1,20 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
@Serializable
|
||||
data class SessionMessagesResponse(
|
||||
val messages: List<SessionMessage>,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SessionMessage(
|
||||
val role: String?,
|
||||
val content: String?,
|
||||
val timestamp: String?,
|
||||
val type: String?,
|
||||
)
|
||||
val role: String? = null,
|
||||
val content: String? = null,
|
||||
val timestamp: JsonElement? = null,
|
||||
val type: String? = null,
|
||||
) {
|
||||
val timestampText: String?
|
||||
get() = (timestamp as? JsonPrimitive)?.content
|
||||
}
|
||||
@@ -1,11 +1,12 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.SerialName
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
@Serializable
|
||||
data class Skill(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val category: String?,
|
||||
val description: String? = null,
|
||||
val category: String? = null,
|
||||
val enabled: Boolean,
|
||||
val content: String? = null,
|
||||
val source: String? = null, // built-in, hub, optional
|
||||
@@ -13,40 +14,46 @@ data class Skill(
|
||||
val linked_files: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class HubSkill(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val source: String?,
|
||||
val description: String? = null,
|
||||
val source: String? = null,
|
||||
val category: String? = null,
|
||||
val identifier: String? = null,
|
||||
@SerializedName("trust_level") val trustLevel: String? = null,
|
||||
@SerialName("trust_level") val trustLevel: String? = null,
|
||||
val repo: String? = null,
|
||||
val tags: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SkillHubSearchResponse(
|
||||
val results: List<HubSkill>,
|
||||
@SerializedName("source_counts") val sourceCounts: Map<String, Int>? = null,
|
||||
@SerializedName("timed_out") val timedOut: List<String>? = null,
|
||||
@SerialName("source_counts") val sourceCounts: Map<String, Int>? = null,
|
||||
@SerialName("timed_out") val timedOut: List<String>? = null,
|
||||
val installed: Map<String, SkillHubInstalledEntry>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SkillHubInstalledEntry(
|
||||
val name: String? = null,
|
||||
@SerializedName("trust_level") val trustLevel: String? = null,
|
||||
@SerializedName("scan_verdict") val scanVerdict: String? = null,
|
||||
@SerialName("trust_level") val trustLevel: String? = null,
|
||||
@SerialName("scan_verdict") val scanVerdict: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SkillHubInstallRequest(
|
||||
val identifier: String,
|
||||
val profile: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SkillHubUninstallRequest(
|
||||
val name: String,
|
||||
val profile: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class SkillScanResponse(
|
||||
val identifier: String,
|
||||
val passed: Boolean? = null,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SkillContentResponse(
|
||||
val name: String,
|
||||
val content: String,
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class StatusResponse(
|
||||
val version: String?,
|
||||
val gateway_running: Boolean?,
|
||||
val active_sessions: Int?,
|
||||
val auth_required: Boolean?,
|
||||
val gateway_platforms: Map<String, PlatformStatus>?,
|
||||
val version: String? = null,
|
||||
val gateway_running: Boolean? = null,
|
||||
val active_sessions: Int? = null,
|
||||
val auth_required: Boolean? = null,
|
||||
val gateway_platforms: Map<String, PlatformStatus?>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class PlatformStatus(
|
||||
val state: String?,
|
||||
val state: String? = null,
|
||||
val error_code: String? = null,
|
||||
)
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class SystemStatsResponse(
|
||||
val os: String? = null,
|
||||
val os_release: String? = null,
|
||||
@@ -20,6 +22,7 @@ data class SystemStatsResponse(
|
||||
val process: ProcessStats? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class MemoryStats(
|
||||
val total: Long? = null,
|
||||
val available: Long? = null,
|
||||
@@ -27,6 +30,7 @@ data class MemoryStats(
|
||||
val percent: Double? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DiskStats(
|
||||
val total: Long? = null,
|
||||
val used: Long? = null,
|
||||
@@ -34,6 +38,7 @@ data class DiskStats(
|
||||
val percent: Double? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ProcessStats(
|
||||
val pid: Int? = null,
|
||||
val rss: Long? = null,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class ToggleSkillRequest(
|
||||
val name: String,
|
||||
val enabled: Boolean,
|
||||
|
||||
@@ -1,15 +1,18 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class Toolset(
|
||||
val name: String,
|
||||
val label: String?,
|
||||
val description: String?,
|
||||
val label: String? = null,
|
||||
val description: String? = null,
|
||||
val enabled: Boolean,
|
||||
val available: Boolean?,
|
||||
val configured: Boolean?,
|
||||
val tools: List<String>?,
|
||||
val available: Boolean? = null,
|
||||
val configured: Boolean? = null,
|
||||
val tools: List<String>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class ToolsetToggleRequest(
|
||||
val enabled: Boolean,
|
||||
val profile: String? = null,
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class UpdateCheckResponse(
|
||||
val install_method: String? = null,
|
||||
val current_version: String? = null,
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
data class WebhooksResponse(
|
||||
val enabled: Boolean,
|
||||
val base_url: String?,
|
||||
val subscriptions: List<WebhookSubscription>?,
|
||||
val base_url: String? = null,
|
||||
val subscriptions: List<WebhookSubscription>? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class WebhookSubscription(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val events: List<String>?,
|
||||
val deliver: String?,
|
||||
val deliver_only: Boolean?,
|
||||
val prompt: String?,
|
||||
val skills: List<String>?,
|
||||
val created_at: String?,
|
||||
val description: String? = null,
|
||||
val events: List<String>? = null,
|
||||
val deliver: String? = null,
|
||||
val deliver_only: Boolean? = null,
|
||||
val prompt: String? = null,
|
||||
val skills: List<String>? = null,
|
||||
val created_at: String? = null,
|
||||
val url: String,
|
||||
val secret_set: Boolean?,
|
||||
val enabled: Boolean?,
|
||||
val secret_set: Boolean? = null,
|
||||
val enabled: Boolean? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class CreateWebhookRequest(
|
||||
val name: String,
|
||||
val description: String? = null,
|
||||
@@ -32,14 +36,17 @@ data class CreateWebhookRequest(
|
||||
val secret: String? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class WebhooksToggleRequest(
|
||||
val enabled: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class WebhookToggleSubscriptionRequest(
|
||||
val enabled: Boolean,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class DeleteWebhookResponse(
|
||||
val ok: Boolean,
|
||||
)
|
||||
@@ -4,9 +4,10 @@ import com.m57.hermescontrol.BuildConfig
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import okhttp3.CertificatePinner
|
||||
import okhttp3.Interceptor
|
||||
import okhttp3.MediaType.Companion.toMediaType
|
||||
import okhttp3.logging.HttpLoggingInterceptor
|
||||
import retrofit2.Retrofit
|
||||
import retrofit2.converter.gson.GsonConverterFactory
|
||||
import retrofit2.converter.kotlinx.serialization.asConverterFactory
|
||||
|
||||
/**
|
||||
* Provides a Retrofit-backed [HermesApiService].
|
||||
@@ -71,7 +72,7 @@ object ApiClient {
|
||||
.Builder()
|
||||
.baseUrl("http://$host:$port/")
|
||||
.client(tempOkHttp)
|
||||
.addConverterFactory(GsonConverterFactory.create(OkHttpProvider.gson))
|
||||
.addConverterFactory(OkHttpProvider.json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
|
||||
return tempRetrofit.create(HermesApiService::class.java)
|
||||
@@ -135,7 +136,7 @@ object ApiClient {
|
||||
.Builder()
|
||||
.baseUrl(AuthManager.baseUrl())
|
||||
.client(okHttp)
|
||||
.addConverterFactory(GsonConverterFactory.create(OkHttpProvider.gson))
|
||||
.addConverterFactory(OkHttpProvider.json.asConverterFactory("application/json".toMediaType()))
|
||||
.build()
|
||||
.also { retrofit = it }
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
package com.m57.hermescontrol.data.remote
|
||||
|
||||
import com.google.gson.JsonElement
|
||||
import com.m57.hermescontrol.data.model.AchievementsResponse
|
||||
import com.m57.hermescontrol.data.model.ActionResponse
|
||||
import com.m57.hermescontrol.data.model.ActionStatusResponse
|
||||
@@ -91,6 +90,7 @@ import com.m57.hermescontrol.data.model.WebhookSubscription
|
||||
import com.m57.hermescontrol.data.model.WebhookToggleSubscriptionRequest
|
||||
import com.m57.hermescontrol.data.model.WebhooksResponse
|
||||
import com.m57.hermescontrol.data.model.WebhooksToggleRequest
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import okhttp3.ResponseBody
|
||||
import retrofit2.Response
|
||||
import retrofit2.http.Body
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.m57.hermescontrol.data.remote
|
||||
|
||||
import com.google.gson.JsonParser
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
import retrofit2.Response
|
||||
import java.io.IOException
|
||||
|
||||
@@ -49,11 +50,8 @@ fun mapHttpError(
|
||||
val detail =
|
||||
if (errorBody != null) {
|
||||
try {
|
||||
val parsed =
|
||||
com.google.gson.JsonParser
|
||||
.parseString(errorBody)
|
||||
.asJsonObject
|
||||
parsed.get("detail")?.asString
|
||||
val parsed = OkHttpProvider.json.parseToJsonElement(errorBody).jsonObject
|
||||
parsed["detail"]?.jsonPrimitive?.content
|
||||
} catch (_: Exception) {
|
||||
null
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
package com.m57.hermescontrol.data.remote
|
||||
|
||||
import com.google.gson.Gson
|
||||
import com.google.gson.GsonBuilder
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.Json
|
||||
import kotlinx.serialization.json.JsonNamingStrategy
|
||||
import okhttp3.ConnectionPool
|
||||
import okhttp3.OkHttpClient
|
||||
import java.util.concurrent.TimeUnit
|
||||
@@ -42,6 +43,10 @@ object OkHttpProvider {
|
||||
.followRedirects(false)
|
||||
.build()
|
||||
|
||||
// Shared Gson — thread-safe, zero-config
|
||||
val gson: Gson = GsonBuilder().create()
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
val json: Json =
|
||||
Json {
|
||||
ignoreUnknownKeys = true
|
||||
namingStrategy = JsonNamingStrategy.SnakeCase
|
||||
}
|
||||
}
|
||||
@@ -1,9 +1,12 @@
|
||||
package com.m57.hermescontrol.data.ws
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
/**
|
||||
* Response from `commands.catalog` RPC — full catalog of available
|
||||
* slash commands, subcommands, and alias mappings.
|
||||
*/
|
||||
@Serializable
|
||||
data class CommandCatalog(
|
||||
val pairs: List<List<String>> = emptyList(),
|
||||
val sub: Map<String, List<String>> = emptyMap(),
|
||||
@@ -16,6 +19,7 @@ data class CommandCatalog(
|
||||
/**
|
||||
* A named category within the command catalog.
|
||||
*/
|
||||
@Serializable
|
||||
data class CommandCategory(
|
||||
val name: String,
|
||||
val pairs: List<List<String>>,
|
||||
|
||||
@@ -24,12 +24,13 @@ object EventParser {
|
||||
return if (response.error != null) {
|
||||
WsEvent.RpcError(id, response.error)
|
||||
} else {
|
||||
WsEvent.RpcResult(id, response.result)
|
||||
WsEvent.RpcResult(id, response.result?.toAny())
|
||||
}
|
||||
}
|
||||
|
||||
// ── Notification / event (no id, has method) ─────────────────────
|
||||
val params = response.params ?: return WsEvent.Unknown(rawJson)
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
val params = response.params?.toAny() as? Map<String, Any?> ?: return WsEvent.Unknown(rawJson)
|
||||
val eventType = params["type"] as? String ?: return WsEvent.Unknown(rawJson)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
|
||||
@@ -24,6 +24,8 @@ import kotlinx.coroutines.flow.flowOn
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.flow.shareIn
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.encodeToString
|
||||
import okhttp3.Request
|
||||
import okhttp3.RequestBody.Companion.toRequestBody
|
||||
import okhttp3.Response
|
||||
@@ -123,7 +125,7 @@ object HermesWsClient {
|
||||
.buffer(Channel.BUFFERED)
|
||||
.map { text ->
|
||||
try {
|
||||
val rpc = OkHttpProvider.gson.fromJson(text, JsonRpcResponse::class.java)
|
||||
val rpc = OkHttpProvider.json.decodeFromString<JsonRpcResponse>(text)
|
||||
EventParser.parse(rpc, text)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to parse message", e)
|
||||
@@ -231,8 +233,8 @@ object HermesWsClient {
|
||||
): String {
|
||||
val id = requestId.incrementAndGet().toString()
|
||||
onSent?.invoke(id)
|
||||
val request = JsonRpcRequest(id = id, method = method, params = params)
|
||||
val json = OkHttpProvider.gson.toJson(request)
|
||||
val request = JsonRpcRequest(id = id, method = method, params = params.mapValues { it.value.toJsonElement() })
|
||||
val json = OkHttpProvider.json.encodeToString(request)
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, "→ $json")
|
||||
val ws = webSocket
|
||||
if (ws != null && connected.get()) {
|
||||
|
||||
@@ -1,32 +1,77 @@
|
||||
package com.m57.hermescontrol.data.ws
|
||||
|
||||
import kotlinx.serialization.Serializable
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
import kotlinx.serialization.json.doubleOrNull
|
||||
import kotlinx.serialization.json.longOrNull
|
||||
|
||||
/**
|
||||
* Outgoing JSON-RPC 2.0 request sent over the WebSocket.
|
||||
*/
|
||||
@Serializable
|
||||
data class JsonRpcRequest(
|
||||
val jsonrpc: String = "2.0",
|
||||
val id: String,
|
||||
val method: String,
|
||||
val params: Map<String, Any> = emptyMap(),
|
||||
val params: Map<String, JsonElement> = emptyMap(),
|
||||
)
|
||||
|
||||
/**
|
||||
* Incoming JSON-RPC 2.0 response **or** server-pushed notification.
|
||||
*
|
||||
* - RPC result/error → [id] is non-null, [result] or [error] is populated.
|
||||
* - Event notification → [id] is null, [method] is `"event"`, event data in [params].
|
||||
*/
|
||||
@Serializable
|
||||
data class JsonRpcResponse(
|
||||
val jsonrpc: String?,
|
||||
val id: String?,
|
||||
val result: Any?,
|
||||
val error: JsonRpcError?,
|
||||
val method: String?,
|
||||
val params: Map<String, Any?>?,
|
||||
val jsonrpc: String? = null,
|
||||
val id: String? = null,
|
||||
val result: JsonElement? = null,
|
||||
val error: JsonRpcError? = null,
|
||||
val method: String? = null,
|
||||
val params: JsonObject? = null,
|
||||
)
|
||||
|
||||
@Serializable
|
||||
data class JsonRpcError(
|
||||
val code: Int,
|
||||
val message: String,
|
||||
val data: Any? = null,
|
||||
val data: JsonElement? = null,
|
||||
)
|
||||
|
||||
fun Any?.toJsonElement(): JsonElement =
|
||||
when (this) {
|
||||
null -> JsonNull
|
||||
is JsonElement -> this
|
||||
is Boolean -> JsonPrimitive(this)
|
||||
is Number -> JsonPrimitive(this)
|
||||
is String -> JsonPrimitive(this)
|
||||
is Map<*, *> -> JsonObject(this.map { it.key.toString() to it.value.toJsonElement() }.toMap())
|
||||
is List<*> -> JsonArray(this.map { it.toJsonElement() })
|
||||
else -> JsonPrimitive(this.toString())
|
||||
}
|
||||
|
||||
fun JsonElement.toAny(): Any? =
|
||||
when (this) {
|
||||
is JsonNull -> {
|
||||
null
|
||||
}
|
||||
|
||||
is JsonPrimitive -> {
|
||||
if (isString) {
|
||||
content
|
||||
} else {
|
||||
booleanOrNull ?: doubleOrNull ?: longOrNull ?: content
|
||||
}
|
||||
}
|
||||
|
||||
is JsonObject -> {
|
||||
mapValues { it.value.toAny() }
|
||||
}
|
||||
|
||||
is JsonArray -> {
|
||||
map { it.toAny() }
|
||||
}
|
||||
}
|
||||
@@ -82,6 +82,7 @@ import androidx.compose.ui.unit.sp
|
||||
import coil.compose.AsyncImage
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.Attachment
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import com.m57.hermescontrol.theme.AssistantBubble
|
||||
import com.m57.hermescontrol.theme.AssistantBubbleLight
|
||||
import com.m57.hermescontrol.theme.StatusGreen
|
||||
@@ -92,6 +93,14 @@ import com.m57.hermescontrol.theme.ToolChipColorLight
|
||||
import com.m57.hermescontrol.theme.UserBubble
|
||||
import kotlinx.coroutines.delay
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonNull
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.boolean
|
||||
import kotlinx.serialization.json.double
|
||||
import kotlinx.serialization.json.int
|
||||
import java.time.Instant
|
||||
import java.time.ZoneId
|
||||
import java.time.format.DateTimeFormatter
|
||||
@@ -537,9 +546,7 @@ fun parseToolOutput(
|
||||
val trimmed = content.trim()
|
||||
if (!trimmed.startsWith("{") && !trimmed.startsWith("[")) return null
|
||||
return try {
|
||||
val element =
|
||||
com.google.gson.JsonParser
|
||||
.parseString(trimmed)
|
||||
val element = OkHttpProvider.json.parseToJsonElement(trimmed)
|
||||
if (!element.isJsonObject) return null
|
||||
val obj = element.asJsonObject
|
||||
|
||||
@@ -2301,3 +2308,23 @@ private fun InlineAttachment(
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Extensions for backward compatibility with Gson API under kotlinx.serialization
|
||||
private val JsonElement.isJsonObject: Boolean get() = this is JsonObject
|
||||
private val JsonElement.asJsonObject: JsonObject get() = this as JsonObject
|
||||
private val JsonElement.isJsonArray: Boolean get() = this is JsonArray
|
||||
private val JsonElement.asJsonArray: JsonArray get() = this as JsonArray
|
||||
private val JsonElement.isJsonNull: Boolean get() = this is JsonNull
|
||||
private val JsonElement.isJsonPrimitive: Boolean get() = this is JsonPrimitive
|
||||
private val JsonElement.asString: String get() = (this as JsonPrimitive).content
|
||||
private val JsonElement.asDouble: Double get() = (this as JsonPrimitive).double
|
||||
private val JsonElement.asInt: Int get() = (this as JsonPrimitive).int
|
||||
private val JsonElement.asBoolean: Boolean get() = (this as JsonPrimitive).boolean
|
||||
|
||||
private fun JsonElement.has(key: String): Boolean = (this as? JsonObject)?.containsKey(key) == true
|
||||
|
||||
private fun JsonObject.entrySet(): Set<Map.Entry<String, JsonElement>> = entries
|
||||
|
||||
private fun JsonObject.keySet(): Set<String> = keys
|
||||
|
||||
private fun JsonArray.size(): Int = size
|
||||
@@ -18,6 +18,7 @@ import com.m57.hermescontrol.data.ws.ConnectionStatus
|
||||
import com.m57.hermescontrol.data.ws.HermesWsClient
|
||||
import com.m57.hermescontrol.data.ws.WsEvent
|
||||
import com.m57.hermescontrol.data.ws.WsMethods
|
||||
import com.m57.hermescontrol.data.ws.toJsonElement
|
||||
import kotlinx.coroutines.CompletableDeferred
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.Job
|
||||
@@ -32,6 +33,7 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.coroutines.withTimeout
|
||||
import kotlinx.serialization.json.decodeFromJsonElement
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
|
||||
private const val TAG = "ChatViewModel"
|
||||
@@ -845,7 +847,9 @@ class ChatViewModel(
|
||||
val body = result.data
|
||||
val platformsStr =
|
||||
body.gateway_platforms?.entries?.joinToString("\n") { (k, v) ->
|
||||
" • **$k**: ${v.state ?: "Unknown"}${if (v.error_code != null) " (Error: ${v.error_code})" else ""}"
|
||||
val status = v?.state ?: "Unknown"
|
||||
val err = v?.error_code?.let { " (Error: $it)" } ?: ""
|
||||
" • **$k**: $status$err"
|
||||
} ?: "No active platforms"
|
||||
addAssistantMessage(
|
||||
"""
|
||||
@@ -983,13 +987,15 @@ class ChatViewModel(
|
||||
|
||||
fun refreshCurrentSession() {
|
||||
val sessionId = _uiState.value.currentSessionId ?: return
|
||||
loadCachedMessages(sessionId)
|
||||
// Skip the REST call if we already have messages — the WebSocket keeps
|
||||
// the UI current. The REST endpoint can 404 on tab switch-back when the
|
||||
// WS session ID doesn't round-trip through the resolver (issue #366),
|
||||
// showing a misleading error banner while the connection is fine.
|
||||
if (_uiState.value.messages.isNotEmpty()) return
|
||||
loadSessionMessages(sessionId)
|
||||
viewModelScope.launch {
|
||||
loadCachedMessages(sessionId).join()
|
||||
// Skip the REST call if we already have messages — the WebSocket keeps
|
||||
// the UI current. The REST endpoint can 404 on tab switch-back when the
|
||||
// WS session ID doesn't round-trip through the resolver (issue #366),
|
||||
// showing a misleading error banner while the connection is fine.
|
||||
if (_uiState.value.messages.isNotEmpty()) return@launch
|
||||
loadSessionMessages(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
fun refreshSettings() {
|
||||
@@ -1004,8 +1010,8 @@ class ChatViewModel(
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
private fun parseCommandCatalog(map: Map<*, *>): CommandCatalog? =
|
||||
try {
|
||||
val json = OkHttpProvider.gson.toJson(map)
|
||||
OkHttpProvider.gson.fromJson(json, CommandCatalog::class.java)
|
||||
val jsonElement = map.toJsonElement()
|
||||
OkHttpProvider.json.decodeFromJsonElement<CommandCatalog>(jsonElement)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to parse command catalog", e)
|
||||
null
|
||||
@@ -1030,23 +1036,25 @@ class ChatViewModel(
|
||||
)
|
||||
}
|
||||
_streamingState.update { StreamingState() }
|
||||
// Step 1: Load cached messages first — instant display
|
||||
loadCachedMessages(sessionId)
|
||||
// Step 2: Resume session on server
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
wsClient.send(
|
||||
WsMethods.SESSION_RESUME,
|
||||
mapOf("session_id" to sessionId),
|
||||
onSent = { id -> trackRequest(id, WsMethods.SESSION_RESUME) },
|
||||
)
|
||||
viewModelScope.launch {
|
||||
// Step 1: Load cached messages first — instant display
|
||||
loadCachedMessages(sessionId).join()
|
||||
// Step 2: Resume session on server
|
||||
launch(Dispatchers.IO) {
|
||||
wsClient.send(
|
||||
WsMethods.SESSION_RESUME,
|
||||
mapOf("session_id" to sessionId),
|
||||
onSent = { id -> trackRequest(id, WsMethods.SESSION_RESUME) },
|
||||
)
|
||||
}
|
||||
// Step 3: Load fresh messages from REST and merge
|
||||
loadSessionMessages(sessionId)
|
||||
// Step 4: Refresh session list to get latest titles
|
||||
loadSessions()
|
||||
}
|
||||
// Step 3: Load fresh messages from REST and merge
|
||||
loadSessionMessages(sessionId)
|
||||
// Step 4: Refresh session list to get latest titles
|
||||
loadSessions()
|
||||
}
|
||||
|
||||
private fun loadCachedMessages(sessionId: String) {
|
||||
private fun loadCachedMessages(sessionId: String): Job =
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val cachedMessages = repo.loadMessages(sessionId)
|
||||
_uiState.update { state ->
|
||||
@@ -1058,7 +1066,6 @@ class ChatViewModel(
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun loadSessionMessages(sessionId: String) {
|
||||
viewModelScope.launch {
|
||||
@@ -1082,7 +1089,7 @@ class ChatViewModel(
|
||||
// Room duplicates when re-loading the same session.
|
||||
val stableId = "rest-$sessionId-$index"
|
||||
// Preserve original timestamp from the API when available
|
||||
val ts = msg.timestamp?.toLongOrNull() ?: System.currentTimeMillis()
|
||||
val ts = msg.timestampText?.toLongOrNull() ?: System.currentTimeMillis()
|
||||
ChatMessage(
|
||||
id = stableId,
|
||||
role = role,
|
||||
|
||||
@@ -2,6 +2,8 @@ package com.m57.hermescontrol.ui.chat
|
||||
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import com.m57.hermescontrol.data.ws.WsEvent
|
||||
import com.m57.hermescontrol.data.ws.toJsonElement
|
||||
import kotlinx.serialization.encodeToString
|
||||
|
||||
/**
|
||||
* Pure state reducer for WebSocket events.
|
||||
@@ -225,7 +227,7 @@ object ChatWsEventReducer {
|
||||
): ReducerResult {
|
||||
val contentJson =
|
||||
event.data?.let {
|
||||
OkHttpProvider.gson.toJson(it)
|
||||
OkHttpProvider.json.encodeToString(it.toJsonElement())
|
||||
} ?: ""
|
||||
val toolMessage =
|
||||
ChatMessage(
|
||||
@@ -271,7 +273,7 @@ object ChatWsEventReducer {
|
||||
): ReducerResult {
|
||||
val contentJson =
|
||||
event.data?.let {
|
||||
OkHttpProvider.gson.toJson(it)
|
||||
OkHttpProvider.json.encodeToString(it.toJsonElement())
|
||||
} ?: ""
|
||||
val messages = state.messages.toMutableList()
|
||||
val toolIdx =
|
||||
|
||||
@@ -53,8 +53,6 @@ import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonPrimitive
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.SchemaField
|
||||
import com.m57.hermescontrol.ui.common.ErrorState
|
||||
@@ -63,6 +61,11 @@ import com.m57.hermescontrol.ui.common.LoadingState
|
||||
import com.m57.hermescontrol.ui.common.NavIcon
|
||||
import com.m57.hermescontrol.ui.common.SearchBar
|
||||
import com.m57.hermescontrol.ui.common.ToastEffect
|
||||
import kotlinx.serialization.json.JsonArray
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
import kotlinx.serialization.json.booleanOrNull
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
@@ -580,7 +583,7 @@ private fun ConfigField(
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
Switch(
|
||||
checked = currentValue?.asBoolean ?: false,
|
||||
checked = (currentValue as? JsonPrimitive)?.booleanOrNull ?: false,
|
||||
onCheckedChange = { onChange(JsonPrimitive(it)) },
|
||||
)
|
||||
}
|
||||
@@ -590,7 +593,7 @@ private fun ConfigField(
|
||||
DropdownField(
|
||||
label = label,
|
||||
options = field.options ?: emptyList(),
|
||||
selectedValue = currentValue?.asString ?: "",
|
||||
selectedValue = (currentValue as? JsonPrimitive)?.content ?: "",
|
||||
onOptionSelected = { onChange(JsonPrimitive(it)) },
|
||||
)
|
||||
if (description != label) {
|
||||
@@ -607,7 +610,7 @@ private fun ConfigField(
|
||||
OutlinedTextField(
|
||||
value =
|
||||
currentValue?.let {
|
||||
if (it.isJsonPrimitive) it.asString else ""
|
||||
if (it is JsonPrimitive) it.content else ""
|
||||
} ?: "",
|
||||
onValueChange = { text ->
|
||||
text.toDoubleOrNull()?.let { doubleVal ->
|
||||
@@ -630,10 +633,10 @@ private fun ConfigField(
|
||||
// String or other type
|
||||
val textValue =
|
||||
currentValue?.let {
|
||||
when {
|
||||
it.isJsonPrimitive -> it.asString
|
||||
it.isJsonObject -> it.toString()
|
||||
it.isJsonArray -> it.toString()
|
||||
when (it) {
|
||||
is JsonPrimitive -> it.content
|
||||
is JsonObject -> it.toString()
|
||||
is JsonArray -> it.toString()
|
||||
else -> ""
|
||||
}
|
||||
} ?: ""
|
||||
@@ -708,20 +711,17 @@ private fun collectUncoveredPaths(
|
||||
val dotPath = if (prefix.isEmpty()) key else "$prefix.$key"
|
||||
// If this exact path is in the schema, it's covered — skip entirely
|
||||
if (dotPath in schemaPaths) continue
|
||||
if (value is com.google.gson.JsonObject) {
|
||||
// Recurse into nested objects to find uncovered leaves
|
||||
val subKeys = value.keySet()
|
||||
if (value is JsonObject) {
|
||||
// Before recursing deeper, check if this whole sub-tree has any schema coverage
|
||||
val hasSchemaCoverage = schemaPaths.any { it == dotPath || it.startsWith("$dotPath.") }
|
||||
if (hasSchemaCoverage) {
|
||||
// Schema covers some sub-paths — recurse to find what's NOT covered
|
||||
val subMap = subKeys.associateWith { value.get(it) }
|
||||
result.addAll(collectUncoveredPaths(subMap, schemaPaths, dotPath))
|
||||
result.addAll(collectUncoveredPaths(value, schemaPaths, dotPath))
|
||||
} else {
|
||||
// No schema coverage at all for this subtree — add the whole thing
|
||||
result.add(dotPath)
|
||||
}
|
||||
} else if (value is com.google.gson.JsonArray) {
|
||||
} else if (value is JsonArray) {
|
||||
// Arrays: check if the path is in schema; if not, add it
|
||||
if (dotPath !in schemaPaths) {
|
||||
result.add(dotPath)
|
||||
@@ -746,7 +746,7 @@ private fun getJsonValue(
|
||||
if (i == 0) {
|
||||
current = config[key]
|
||||
} else {
|
||||
current = current?.asJsonObject?.get(key)
|
||||
current = (current as? JsonObject)?.get(key)
|
||||
}
|
||||
if (current == null) return null
|
||||
}
|
||||
|
||||
@@ -2,8 +2,6 @@ package com.m57.hermescontrol.ui.config
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.JsonElement
|
||||
import com.google.gson.JsonObject
|
||||
import com.m57.hermescontrol.data.model.ConfigSchemaResponse
|
||||
import com.m57.hermescontrol.data.model.ConfigUpdateRequest
|
||||
import com.m57.hermescontrol.data.model.UpdateRawConfigRequest
|
||||
@@ -19,6 +17,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.JsonElement
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
|
||||
data class ConfigUiState(
|
||||
val isLoading: Boolean = false,
|
||||
@@ -296,15 +296,14 @@ class ConfigViewModel :
|
||||
val topKey = parts.first()
|
||||
val rest = parts.drop(1).joinToString(".")
|
||||
val topValue = config[topKey]
|
||||
val nestedJson = topValue?.asJsonObject ?: JsonObject()
|
||||
val nestedJson = (topValue as? JsonObject) ?: JsonObject(emptyMap())
|
||||
val updatedNested =
|
||||
applyNestedValue(
|
||||
nestedJson.entrySet().associate { it.key to it.value },
|
||||
nestedJson,
|
||||
rest,
|
||||
value,
|
||||
)
|
||||
val nestedObj = JsonObject()
|
||||
updatedNested.forEach { (k, v) -> nestedObj.add(k, v) }
|
||||
val nestedObj = JsonObject(updatedNested)
|
||||
val mutable = config.toMutableMap()
|
||||
mutable[topKey] = nestedObj
|
||||
return mutable
|
||||
@@ -312,23 +311,38 @@ class ConfigViewModel :
|
||||
|
||||
/** Build a nested JSON changeset from dot-path pending changes. */
|
||||
private fun buildChangeset(changes: Map<String, JsonElement>): Map<String, JsonElement> {
|
||||
val root = JsonObject()
|
||||
val root = mutableMapOf<String, Any>()
|
||||
for ((dotPath, value) in changes) {
|
||||
val parts = dotPath.split(".")
|
||||
var current = root
|
||||
for (i in 0 until parts.size - 1) {
|
||||
val key = parts[i]
|
||||
val existing = current.get(key)
|
||||
if (existing == null || !existing.isJsonObject) {
|
||||
val newObj = JsonObject()
|
||||
current.add(key, newObj)
|
||||
current = newObj
|
||||
val existing = current[key]
|
||||
if (existing !is MutableMap<*, *>) {
|
||||
val newMap = mutableMapOf<String, Any>()
|
||||
current[key] = newMap
|
||||
current = newMap
|
||||
} else {
|
||||
current = existing.asJsonObject
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
current = existing as MutableMap<String, Any>
|
||||
}
|
||||
}
|
||||
current.add(parts.last(), value)
|
||||
current[parts.last()] = value
|
||||
}
|
||||
return root.entrySet().associate { it.key to it.value as JsonElement }
|
||||
|
||||
fun toJsonObject(map: Map<String, Any>): JsonObject {
|
||||
val content =
|
||||
map.mapValues { (_, v) ->
|
||||
if (v is Map<*, *>) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
toJsonObject(v as Map<String, Any>)
|
||||
} else {
|
||||
v as JsonElement
|
||||
}
|
||||
}
|
||||
return JsonObject(content)
|
||||
}
|
||||
|
||||
return toJsonObject(root)
|
||||
}
|
||||
}
|
||||
@@ -9,6 +9,7 @@ import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkError
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -17,6 +18,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
data class ConnectUiState(
|
||||
val token: String = "",
|
||||
@@ -317,13 +320,10 @@ class ConnectViewModel(
|
||||
val decodedBytes = android.util.Base64.decode(trimmed, android.util.Base64.DEFAULT)
|
||||
val decodedString = String(decodedBytes, Charsets.UTF_8)
|
||||
if (decodedString.startsWith("{") && decodedString.endsWith("}")) {
|
||||
val json =
|
||||
com.google.gson.JsonParser
|
||||
.parseString(decodedString)
|
||||
.asJsonObject
|
||||
val host = json.get("host")?.asString
|
||||
val port = json.get("port")?.asInt?.toString() ?: json.get("port")?.asString
|
||||
val token = json.get("token")?.asString
|
||||
val json = OkHttpProvider.json.parseToJsonElement(decodedString).jsonObject
|
||||
val host = json["host"]?.jsonPrimitive?.content
|
||||
val port = json["port"]?.jsonPrimitive?.content
|
||||
val token = json["token"]?.jsonPrimitive?.content
|
||||
if (host != null && port != null && token != null) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
|
||||
@@ -8,6 +8,7 @@ import com.m57.hermescontrol.data.model.UpdateCronJobRequest
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import com.m57.hermescontrol.data.ws.toJsonElement
|
||||
import com.m57.hermescontrol.ui.common.ToastHost
|
||||
import com.m57.hermescontrol.ui.common.safeLaunchLoad
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
@@ -18,6 +19,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.JsonObject
|
||||
import kotlinx.serialization.json.JsonPrimitive
|
||||
|
||||
data class CronJobsUiState(
|
||||
val isLoading: Boolean = false,
|
||||
@@ -292,7 +295,7 @@ class CronJobsViewModel :
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.updateCronJob(
|
||||
editor.jobId ?: "",
|
||||
UpdateCronJobRequest(updates = updates),
|
||||
UpdateCronJobRequest(updates = updates.mapValues { it.value.toJsonElement() }),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -336,8 +339,8 @@ class CronJobsViewModel :
|
||||
private fun extractScheduleString(job: CronJob): String {
|
||||
val s = job.schedule
|
||||
return when (s) {
|
||||
is String -> s
|
||||
is Map<*, *> -> (s["value"] as? String) ?: job.scheduleText
|
||||
is JsonPrimitive -> s.content
|
||||
is JsonObject -> (s["value"] as? JsonPrimitive)?.content ?: job.scheduleText
|
||||
else -> job.scheduleText
|
||||
}
|
||||
}
|
||||
|
||||
@@ -267,7 +267,7 @@ fun GatewayScreen(
|
||||
style = MaterialTheme.typography.bodyLarge,
|
||||
fontWeight = FontWeight.Medium,
|
||||
)
|
||||
val stateText = pStatus.state?.uppercase() ?: "UNKNOWN"
|
||||
val stateText = pStatus?.state?.uppercase() ?: "UNKNOWN"
|
||||
val badgeColor =
|
||||
when (stateText) {
|
||||
"RUNNING" -> Color(0xFF4CAF50)
|
||||
|
||||
@@ -6,12 +6,12 @@ import android.util.Log
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.ViewModelProvider
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.google.gson.JsonParser
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.local.AuthManager
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkError
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
import com.m57.hermescontrol.data.remote.OkHttpProvider
|
||||
import com.m57.hermescontrol.data.remote.safeApiCall
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.MutableStateFlow
|
||||
@@ -20,6 +20,8 @@ import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.jsonObject
|
||||
import kotlinx.serialization.json.jsonPrimitive
|
||||
|
||||
/**
|
||||
* UI state for the pairing code entry screen.
|
||||
@@ -88,12 +90,10 @@ class PairingCodeEntryViewModel(
|
||||
val decodedBytes = Base64.decode(trimmed, Base64.DEFAULT)
|
||||
val decodedString = String(decodedBytes, Charsets.UTF_8)
|
||||
if (decodedString.startsWith("{") && decodedString.endsWith("}")) {
|
||||
val json = JsonParser.parseString(decodedString).asJsonObject
|
||||
val host = json.get("host")?.asString
|
||||
val port =
|
||||
json.get("port")?.asInt?.toString()
|
||||
?: json.get("port")?.asString
|
||||
val token = json.get("token")?.asString
|
||||
val json = OkHttpProvider.json.parseToJsonElement(decodedString).jsonObject
|
||||
val host = json["host"]?.jsonPrimitive?.content
|
||||
val port = json["port"]?.jsonPrimitive?.content
|
||||
val token = json["token"]?.jsonPrimitive?.content
|
||||
// Accept raw "host" + "port" + "token", or a composite "pairing" payload
|
||||
if (host != null && port != null && token != null) {
|
||||
connectToDashboard(host, port.toIntOrNull() ?: 9119, token)
|
||||
|
||||
@@ -786,7 +786,7 @@ private fun LazyListScope.gatewaySection(
|
||||
style = MaterialTheme.typography.bodyMedium,
|
||||
modifier = Modifier.weight(1f),
|
||||
)
|
||||
platform.state?.let { platformState ->
|
||||
platform?.state?.let { platformState ->
|
||||
StatusBadge(
|
||||
text = platformState,
|
||||
status =
|
||||
|
||||
Reference in new issue
Block a user