diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/Profile.kt b/app/src/main/java/com/m57/hermescontrol/data/model/Profile.kt index 53248d6e..f8721ec5 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/Profile.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/Profile.kt @@ -47,11 +47,6 @@ data class UpdateProfileModelRequest( val model: String, ) -@Serializable -data class CloneProfileRequest( - val name: String, -) - @Serializable data class UpdateProfileDescriptionRequest( val description: String, @@ -89,6 +84,9 @@ data class CreateProfileRequest( val mcp_servers: List? = null, val keep_skills: Boolean? = null, val hub_skills: List? = null, + val clone_from: String? = null, + val clone_all: Boolean? = null, + val clone_from_default: Boolean? = null, ) @Serializable diff --git a/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt b/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt index a7a0e1fa..c3c479bf 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/model/SessionListResponse.kt @@ -60,9 +60,3 @@ data class BulkDeleteResponse( data class PruneRequest( val days: Int, ) - -@Serializable -data class SessionPromptResponse( - val prompt: String? = null, - val id: String? = null, -) diff --git a/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt b/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt index 81eb673c..66b8fe99 100644 --- a/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt +++ b/app/src/main/java/com/m57/hermescontrol/data/remote/HermesApiService.kt @@ -12,7 +12,6 @@ import com.m57.hermescontrol.data.model.BackupTriggerRequest import com.m57.hermescontrol.data.model.BulkDeleteRequest import com.m57.hermescontrol.data.model.BulkDeleteResponse 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.ConfigUpdateRequest 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.SessionListResponse 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.SessionSearchResponse import com.m57.hermescontrol.data.model.SessionStatsResponse @@ -244,13 +242,6 @@ interface HermesApiService { @Path("id", encoded = true) sessionId: String, ): Response - @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 - @GET("api/sessions/{id}") suspend fun getSessionDetail( // Preserve slashes in session IDs — backend generates IDs containing '/' characters (issue #468). @@ -407,12 +398,6 @@ interface HermesApiService { @Body body: UpdateProfileModelRequest, ): Response - @POST("api/profiles/{name}/clone") - suspend fun cloneProfile( - @Path("name") name: String, - @Body body: CloneProfileRequest, - ): Response - @PUT("api/profiles/{name}/description") suspend fun updateProfileDescription( @Path("name") name: String, @@ -541,11 +526,6 @@ interface HermesApiService { @Body body: Map, ): Response - @POST("api/mcp/servers/{name}/restart") - suspend fun restartMcpServer( - @Path("name") name: String, - ): Response - @POST("api/mcp/servers/{name}/auth") suspend fun authMcpServer( @Path("name") name: String, diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt index c294de5d..9e02924a 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersScreen.kt @@ -25,7 +25,6 @@ import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Delete 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.Storage import androidx.compose.material3.Button @@ -564,15 +563,6 @@ private fun ServerCard( Spacer(modifier = Modifier.width(spacing.xs)) 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) }) { Icon(Icons.Filled.Delete, contentDescription = stringResource(R.string.action_delete)) } diff --git a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt index d86d52d8..81bdd570 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/mcp/McpServersViewModel.kt @@ -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 ────────────────────────────────────── fun toggleAddForm() { diff --git a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt index cc92636b..e02bd971 100644 --- a/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt +++ b/app/src/main/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModel.kt @@ -3,7 +3,6 @@ package com.m57.hermescontrol.ui.profiles import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope 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.HubSkill import com.m57.hermescontrol.data.model.ModelProvider @@ -242,9 +241,11 @@ class ProfilesViewModel( val result = withContext(ioDispatcher) { safeApiCall { - ApiClient.hermesApi.cloneProfile( - sourceProfileName, - CloneProfileRequest(newProfileName), + ApiClient.hermesApi.createProfile( + CreateProfileRequest( + name = newProfileName, + clone_from = sourceProfileName, + ), ) } } diff --git a/app/src/test/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModelTest.kt b/app/src/test/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModelTest.kt index 7bd2492c..bfe1736b 100644 --- a/app/src/test/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModelTest.kt +++ b/app/src/test/java/com/m57/hermescontrol/ui/profiles/ProfilesViewModelTest.kt @@ -2,6 +2,7 @@ package com.m57.hermescontrol.ui.profiles import com.m57.hermescontrol.data.local.AuthManager 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.ModelProvider import com.m57.hermescontrol.data.model.PinnedModel @@ -311,4 +312,37 @@ class ProfilesViewModelTest { assertNull(vm.uiState.value.activeProfileName) 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) + } }