Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
bd75a6d8ea
commit
d6892cde7f
7 files changed
+42
-65
No files matched your search
@@ -47,11 +47,6 @@ data class UpdateProfileModelRequest(
|
|||||||
val model: String,
|
val model: String,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class CloneProfileRequest(
|
|
||||||
val name: String,
|
|
||||||
)
|
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UpdateProfileDescriptionRequest(
|
data class UpdateProfileDescriptionRequest(
|
||||||
val description: String,
|
val description: String,
|
||||||
@@ -89,6 +84,9 @@ data class CreateProfileRequest(
|
|||||||
val mcp_servers: List<McpServerConfigInput>? = null,
|
val mcp_servers: List<McpServerConfigInput>? = null,
|
||||||
val keep_skills: Boolean? = null,
|
val keep_skills: Boolean? = null,
|
||||||
val hub_skills: List<String>? = null,
|
val hub_skills: List<String>? = null,
|
||||||
|
val clone_from: String? = null,
|
||||||
|
val clone_all: Boolean? = null,
|
||||||
|
val clone_from_default: Boolean? = null,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
|
|||||||
@@ -60,9 +60,3 @@ data class BulkDeleteResponse(
|
|||||||
data class PruneRequest(
|
data class PruneRequest(
|
||||||
val days: Int,
|
val days: Int,
|
||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
|
||||||
data class SessionPromptResponse(
|
|
||||||
val prompt: String? = null,
|
|
||||||
val id: String? = null,
|
|
||||||
)
|
|
||||||
@@ -12,7 +12,6 @@ import com.m57.hermescontrol.data.model.BackupTriggerRequest
|
|||||||
import com.m57.hermescontrol.data.model.BulkDeleteRequest
|
import com.m57.hermescontrol.data.model.BulkDeleteRequest
|
||||||
import com.m57.hermescontrol.data.model.BulkDeleteResponse
|
import com.m57.hermescontrol.data.model.BulkDeleteResponse
|
||||||
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
|
||||||
@@ -95,7 +94,6 @@ import com.m57.hermescontrol.data.model.ScanStatus
|
|||||||
import com.m57.hermescontrol.data.model.SessionDetailResponse
|
import com.m57.hermescontrol.data.model.SessionDetailResponse
|
||||||
import com.m57.hermescontrol.data.model.SessionListResponse
|
import com.m57.hermescontrol.data.model.SessionListResponse
|
||||||
import com.m57.hermescontrol.data.model.SessionMessagesResponse
|
import com.m57.hermescontrol.data.model.SessionMessagesResponse
|
||||||
import com.m57.hermescontrol.data.model.SessionPromptResponse
|
|
||||||
import com.m57.hermescontrol.data.model.SessionRenameRequest
|
import com.m57.hermescontrol.data.model.SessionRenameRequest
|
||||||
import com.m57.hermescontrol.data.model.SessionSearchResponse
|
import com.m57.hermescontrol.data.model.SessionSearchResponse
|
||||||
import com.m57.hermescontrol.data.model.SessionStatsResponse
|
import com.m57.hermescontrol.data.model.SessionStatsResponse
|
||||||
@@ -244,13 +242,6 @@ interface HermesApiService {
|
|||||||
@Path("id", encoded = true) sessionId: String,
|
@Path("id", encoded = true) sessionId: String,
|
||||||
): Response<LatestDescendantResponse>
|
): Response<LatestDescendantResponse>
|
||||||
|
|
||||||
@GET("api/sessions/{id}/prompt")
|
|
||||||
suspend fun getSessionPrompt(
|
|
||||||
// Preserve slashes in session IDs — backend generates IDs containing '/' characters (issue #468).
|
|
||||||
// Contract: The server-generated sessionId must only contain URL-safe characters (no ?, #, or spaces).
|
|
||||||
@Path("id", encoded = true) sessionId: String,
|
|
||||||
): Response<SessionPromptResponse>
|
|
||||||
|
|
||||||
@GET("api/sessions/{id}")
|
@GET("api/sessions/{id}")
|
||||||
suspend fun getSessionDetail(
|
suspend fun getSessionDetail(
|
||||||
// Preserve slashes in session IDs — backend generates IDs containing '/' characters (issue #468).
|
// Preserve slashes in session IDs — backend generates IDs containing '/' characters (issue #468).
|
||||||
@@ -407,12 +398,6 @@ 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")
|
@PUT("api/profiles/{name}/description")
|
||||||
suspend fun updateProfileDescription(
|
suspend fun updateProfileDescription(
|
||||||
@Path("name") name: String,
|
@Path("name") name: String,
|
||||||
@@ -541,11 +526,6 @@ interface HermesApiService {
|
|||||||
@Body body: Map<String, Any>,
|
@Body body: Map<String, Any>,
|
||||||
): Response<McpServer>
|
): Response<McpServer>
|
||||||
|
|
||||||
@POST("api/mcp/servers/{name}/restart")
|
|
||||||
suspend fun restartMcpServer(
|
|
||||||
@Path("name") name: String,
|
|
||||||
): Response<Unit>
|
|
||||||
|
|
||||||
@POST("api/mcp/servers/{name}/auth")
|
@POST("api/mcp/servers/{name}/auth")
|
||||||
suspend fun authMcpServer(
|
suspend fun authMcpServer(
|
||||||
@Path("name") name: String,
|
@Path("name") name: String,
|
||||||
|
|||||||
@@ -25,7 +25,6 @@ import androidx.compose.material.icons.filled.Add
|
|||||||
import androidx.compose.material.icons.filled.Close
|
import androidx.compose.material.icons.filled.Close
|
||||||
import androidx.compose.material.icons.filled.Delete
|
import androidx.compose.material.icons.filled.Delete
|
||||||
import androidx.compose.material.icons.filled.Key
|
import androidx.compose.material.icons.filled.Key
|
||||||
import androidx.compose.material.icons.filled.RestartAlt
|
|
||||||
import androidx.compose.material.icons.filled.Science
|
import androidx.compose.material.icons.filled.Science
|
||||||
import androidx.compose.material.icons.filled.Storage
|
import androidx.compose.material.icons.filled.Storage
|
||||||
import androidx.compose.material3.Button
|
import androidx.compose.material3.Button
|
||||||
@@ -564,15 +563,6 @@ private fun ServerCard(
|
|||||||
Spacer(modifier = Modifier.width(spacing.xs))
|
Spacer(modifier = Modifier.width(spacing.xs))
|
||||||
Text(stringResource(R.string.mcp_servers_action_test), maxLines = 1)
|
Text(stringResource(R.string.mcp_servers_action_test), maxLines = 1)
|
||||||
}
|
}
|
||||||
FilledTonalButton(
|
|
||||||
onClick = { viewModel.restartServer(server.name) },
|
|
||||||
modifier = Modifier.weight(1f),
|
|
||||||
contentPadding = PaddingValues(horizontal = 12.dp, vertical = 10.dp),
|
|
||||||
) {
|
|
||||||
Icon(Icons.Filled.RestartAlt, contentDescription = null, modifier = Modifier.size(16.dp))
|
|
||||||
Spacer(modifier = Modifier.width(spacing.xs))
|
|
||||||
Text(stringResource(R.string.mcp_servers_action_restart), maxLines = 1)
|
|
||||||
}
|
|
||||||
IconButton(onClick = { viewModel.deleteServer(server.name) }) {
|
IconButton(onClick = { viewModel.deleteServer(server.name) }) {
|
||||||
Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.action_delete))
|
Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.action_delete))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -162,26 +162,6 @@ class McpServersViewModel :
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun restartServer(name: String) {
|
|
||||||
viewModelScope.launch {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Restarting server '$name'…") }
|
|
||||||
val result =
|
|
||||||
withContext(Dispatchers.IO) {
|
|
||||||
safeApiCall { ApiClient.hermesApi.restartMcpServer(name) }
|
|
||||||
}
|
|
||||||
when (result) {
|
|
||||||
is NetworkResult.Success -> {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Server '$name' restarted") }
|
|
||||||
loadServers()
|
|
||||||
}
|
|
||||||
|
|
||||||
is NetworkResult.Failure -> {
|
|
||||||
_uiState.update { it.copy(toastMessage = "Failed to restart server: ${result.error.message}") }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Add server form ──────────────────────────────────────
|
// ── Add server form ──────────────────────────────────────
|
||||||
|
|
||||||
fun toggleAddForm() {
|
fun toggleAddForm() {
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ 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.local.AuthManager
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.model.CloneProfileRequest
|
|
||||||
import com.m57.hermescontrol.data.model.CreateProfileRequest
|
import com.m57.hermescontrol.data.model.CreateProfileRequest
|
||||||
import com.m57.hermescontrol.data.model.HubSkill
|
import com.m57.hermescontrol.data.model.HubSkill
|
||||||
import com.m57.hermescontrol.data.model.ModelProvider
|
import com.m57.hermescontrol.data.model.ModelProvider
|
||||||
@@ -242,9 +241,11 @@ class ProfilesViewModel(
|
|||||||
val result =
|
val result =
|
||||||
withContext(ioDispatcher) {
|
withContext(ioDispatcher) {
|
||||||
safeApiCall {
|
safeApiCall {
|
||||||
ApiClient.hermesApi.cloneProfile(
|
ApiClient.hermesApi.createProfile(
|
||||||
sourceProfileName,
|
CreateProfileRequest(
|
||||||
CloneProfileRequest(newProfileName),
|
name = newProfileName,
|
||||||
|
clone_from = sourceProfileName,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.profiles
|
|||||||
|
|
||||||
import com.m57.hermescontrol.data.local.AuthManager
|
import com.m57.hermescontrol.data.local.AuthManager
|
||||||
import com.m57.hermescontrol.data.model.ActiveProfileResponse
|
import com.m57.hermescontrol.data.model.ActiveProfileResponse
|
||||||
|
import com.m57.hermescontrol.data.model.CreateProfileRequest
|
||||||
import com.m57.hermescontrol.data.model.ModelOptionsResponse
|
import com.m57.hermescontrol.data.model.ModelOptionsResponse
|
||||||
import com.m57.hermescontrol.data.model.ModelProvider
|
import com.m57.hermescontrol.data.model.ModelProvider
|
||||||
import com.m57.hermescontrol.data.model.PinnedModel
|
import com.m57.hermescontrol.data.model.PinnedModel
|
||||||
@@ -311,4 +312,37 @@ class ProfilesViewModelTest {
|
|||||||
assertNull(vm.uiState.value.activeProfileName)
|
assertNull(vm.uiState.value.activeProfileName)
|
||||||
assertTrue(vm.uiState.value.toastMessage!!.contains("Failed to switch profile"))
|
assertTrue(vm.uiState.value.toastMessage!!.contains("Failed to switch profile"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cloneProfile success calls createProfile with clone_from and reloads`() {
|
||||||
|
coEvery { mockApi.createProfile(any()) } returns Response.success(Unit)
|
||||||
|
|
||||||
|
val vm = createViewModel()
|
||||||
|
vm.cloneProfile("default", "dev-copy")
|
||||||
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
coVerify {
|
||||||
|
mockApi.createProfile(
|
||||||
|
CreateProfileRequest(
|
||||||
|
name = "dev-copy",
|
||||||
|
clone_from = "default",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
assertTrue(vm.uiState.value.toastMessage!!.contains("cloned successfully"))
|
||||||
|
assertFalse(vm.uiState.value.isLoading)
|
||||||
|
coVerify { mockApi.getProfiles() }
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
fun `cloneProfile failure surfaces toast and stops loading`() {
|
||||||
|
coEvery { mockApi.createProfile(any()) } returns errorResponse(400)
|
||||||
|
|
||||||
|
val vm = createViewModel()
|
||||||
|
vm.cloneProfile("default", "dev-copy")
|
||||||
|
testDispatcher.scheduler.advanceUntilIdle()
|
||||||
|
|
||||||
|
assertTrue(vm.uiState.value.toastMessage!!.contains("Failed to clone profile"))
|
||||||
|
assertFalse(vm.uiState.value.isLoading)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user