feat(#441): add profile cloning and description editing (#463)

* feat(#441): add profile cloning and description editing

* feat(#441): add profile builder wizard and API enhancements

Introduce 5-step Profile Builder wizard into ProfilesScreen and add support for create/clone endpoints.

* fix(#441): restore imports and add ProfileInfo constructor defaults

* style: run ktlint format on codebase to fix styling errors

* fix(#441): address PR review comments including provider slug bug, validation, and strings

* fix(#441): remove unused McpServerConfigInput import

---------

Co-authored-by: Hermes Agent <hermes@agent.local>
This commit is contained in:
M57andHermes Agent authored and GitHub committed 2026-07-03 22:24:42 +03:00
1 parent 846a9380e2
commit d181851670
37 files changed
+2299 -448

No files matched your search

File diff suppressed because it is too large. Load diff
@@ -2,8 +2,14 @@ package com.m57.hermescontrol.ui.profiles
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.m57.hermescontrol.data.model.CloneProfileRequest
import com.m57.hermescontrol.data.model.CreateProfileRequest
import com.m57.hermescontrol.data.model.HubSkill
import com.m57.hermescontrol.data.model.ModelProvider
import com.m57.hermescontrol.data.model.ProfileInfo
import com.m57.hermescontrol.data.model.SetActiveProfileRequest
import com.m57.hermescontrol.data.model.Skill
import com.m57.hermescontrol.data.model.UpdateProfileDescriptionRequest
import com.m57.hermescontrol.data.model.UpdateProfileModelRequest
import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest
import com.m57.hermescontrol.data.remote.ApiClient
@@ -26,9 +32,17 @@ data class ProfilesUiState(
val isLoadingSoul: Boolean = false,
val errorMessage: String? = null,
val toastMessage: String? = null,
// Profile Builder states
val modelProviders: List<ModelProvider> = emptyList(),
val isLoadingBuilderData: Boolean = false,
val availableSkills: List<Skill> = emptyList(),
val hubSearchResults: List<HubSkill> = emptyList(),
val isSearchingHub: Boolean = false,
)
class ProfilesViewModel : ViewModel(), ToastHost {
class ProfilesViewModel :
ViewModel(),
ToastHost {
private val _uiState = MutableStateFlow(ProfilesUiState())
val uiState: StateFlow<ProfilesUiState> = _uiState.asStateFlow()
@@ -197,4 +211,174 @@ class ProfilesViewModel : ViewModel(), ToastHost {
fun closeSoulDialog() {
_uiState.update { it.copy(selectedSoulContent = null) }
}
fun cloneProfile(
sourceProfileName: String,
newProfileName: String,
) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
val result =
withContext(Dispatchers.IO) {
safeApiCall {
ApiClient.hermesApi.cloneProfile(
sourceProfileName,
CloneProfileRequest(newProfileName),
)
}
}
when (result) {
is NetworkResult.Success -> {
_uiState.update {
it.copy(
toastMessage = "Profile '$sourceProfileName' cloned successfully to '$newProfileName'",
)
}
loadProfiles()
}
is NetworkResult.Failure -> {
_uiState.update {
it.copy(
isLoading = false,
toastMessage = "Failed to clone profile '$sourceProfileName': ${result.error.message}",
)
}
}
}
}
}
fun updateProfileDescription(
profileName: String,
description: String,
) {
viewModelScope.launch {
_uiState.update { it.copy(isLoading = true) }
val result =
withContext(Dispatchers.IO) {
safeApiCall {
ApiClient.hermesApi.updateProfileDescription(
profileName,
UpdateProfileDescriptionRequest(description),
)
}
}
when (result) {
is NetworkResult.Success -> {
_uiState.update {
it.copy(
toastMessage = "Profile '$profileName' description updated",
)
}
loadProfiles()
}
is NetworkResult.Failure -> {
_uiState.update {
it.copy(
isLoading = false,
toastMessage = "Failed to update description for '$profileName': ${result.error.message}",
)
}
}
}
}
}
fun loadBuilderData() {
_uiState.update { it.copy(isLoadingBuilderData = true, errorMessage = null) }
viewModelScope.launch {
val modelsResult =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getModelOptions() }
}
val skillsResult =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.getSkills() }
}
if (modelsResult is NetworkResult.Success && skillsResult is NetworkResult.Success) {
_uiState.update {
it.copy(
isLoadingBuilderData = false,
modelProviders = modelsResult.data.providers,
availableSkills = skillsResult.data,
)
}
} else {
val modelsError = (modelsResult as? NetworkResult.Failure)?.error?.message ?: "Success"
val skillsError = (skillsResult as? NetworkResult.Failure)?.error?.message ?: "Success"
_uiState.update {
it.copy(
isLoadingBuilderData = false,
errorMessage = "Failed to load builder data: Models: $modelsError, Skills: $skillsError",
)
}
}
}
}
fun searchHub(query: String) {
if (query.isBlank()) return
_uiState.update { it.copy(isSearchingHub = true) }
viewModelScope.launch {
val result =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.searchSkillsHub(query) }
}
when (result) {
is NetworkResult.Success -> {
_uiState.update {
it.copy(
isSearchingHub = false,
hubSearchResults = result.data,
)
}
}
is NetworkResult.Failure -> {
_uiState.update {
it.copy(
isSearchingHub = false,
toastMessage = "Failed to search skills hub: ${result.error.message}",
)
}
}
}
}
}
fun createProfile(
request: CreateProfileRequest,
onSuccess: () -> Unit,
) {
_uiState.update { it.copy(isLoading = true) }
viewModelScope.launch {
val result =
withContext(Dispatchers.IO) {
safeApiCall { ApiClient.hermesApi.createProfile(request) }
}
when (result) {
is NetworkResult.Success -> {
_uiState.update {
it.copy(
toastMessage = "Profile ${request.name} created successfully",
)
}
loadProfiles()
onSuccess()
}
is NetworkResult.Failure -> {
_uiState.update {
it.copy(
isLoading = false,
toastMessage = "Failed to create profile: ${result.error.message}",
)
}
}
}
}
}
}