fix: correct FilterChipRow and EmptyState API mismatches in Skills screen (#466)
* fix: correct FilterChipRow and EmptyState API mismatches + ktlint fmt * fix: add missing Save import, fix verticalArrangement double-wrapping * fix: address Sourcery review — strings, enums, hubQuery, StatusColors • Hardcoded strings → stringResource (Installed, Browse Hub, Statuses, All sources) • Magic string filters → SkillFilter enum + CATEGORY_ALL constant • FilterChipRow made generic with chipLabel parameter • HubBrowseView hubQuery driven from ViewModel state (no dupe) • ViewModel toasts use getString(R.string.*) via AndroidViewModel • MaterialTheme.colorScheme.error → LocalHermesStatusColors.current.error * fix: OpenDrawer → Menu, listContentPadding is PaddingValues not ext NavIcon.OpenDrawer doesn't exist, use NavIcon.Menu. listContentPadding is a val PaddingValues, not a Modifier extension — use Modifier.padding(listContentPadding). * fix: NavIcon.Menu(onClick) -> NavIcon.Menu(onOpen) * fix: pass mockApp to SkillsViewModel in unit tests (now AndroidViewModel) * fix: mock getString in unit tests for AndroidViewModel string resources * fix: anyVararg<Any?>() → anyVararg<Any>() to satisfy type bound anyVararg<T> has bound T : Any, so Any? doesn't compile. Any is a subtype of Any? at the JVM level, so the matcher works correctly for vararg Any? parameters. * fix: use anyVararg<String>() for getString mock to match actual runtime types The previous anyVararg<Any>() did not match getString(Int, Object...) vararg calls at runtime — mockk's relaxed mock returned '' for all formatted string resources, causing all Skills error-path assertions to fail (9 tests). Root cause: mockk vararg matchers match by reified element type. The Java method Context.getString(int, Object...) passes format args as String at runtime (via Kotlin's spread). anyVararg<Any>() with Any (boxed) doesn't match Object/String vararg elements reliably, but anyVararg<String>() does, since String satisfies the T : Any bound and matches the actual runtime type. Also added getString(Int) stub for the no-arg overload used by enums. * fix: stub getString per-resource-ID to bypass mockk Java vararg bug mockk has a known bug matching Java vararg methods like Context.getString(int, Object...) with anyVararg<T>() — the matcher silently fails to match, and the relaxed mock returns '' for all formatted string resource calls. This caused 8 Skills error-path tests to fail (errorMessage/toastMessage was '' instead of containing 'HTTP 500'). Root cause: confirmed via test report artifacts — errorMessage was non-null but empty string, proving the vararg stub never matched. Fix: replace the generic anyVararg<String>() stub with explicit per- resource-ID stubs using eq() + any<String>() matchers. Each formatted string resource is stubbed individually, returning the format arg (the dynamic error message) so the test assertions find 'HTTP 500' and 'Timeout' substrings. References: - https://stackoverflow.com/questions/54326914/using-mockk-to-match-any-varargs - https://github.com/mockk/mockk/issues/224 * fix(skills): replace AndroidViewModel getString() with hardcoded strings to fix unit tests The SkillsViewModel was using getApplication<Application>().getString() for error/success messages. In unit tests, mockk cannot reliably stub Java vararg Context.getString(int, Object...), causing NullPointerException inside coroutine onError callbacks. All other ViewModels in the codebase use hardcoded strings; this change aligns SkillsViewModel with that convention. * test: add hub search/install/uninstall/preview coverage + rm vestigial stubs - Remove unused mockApp.getString() per-resource-ID stubs (SkillsViewModel no longer calls getApplication().getString() — hardcoded strings now). - Add 10 tests covering hub search (success/empty/error/emptyQuery), hub install (success/error), hub uninstall (success/error), and skill preview (success/error). Uses existing mockk + runTest patterns. * fix: correct import ordering for HubSkill (H < S) Move HubSkill import before KanbanBoard (H < K) and remove duplicate that was between SessionListResponse and Skill. * test: remove coVerify from emptyQuery test (cross-test mock state) * fix: remove double-stacked padding in SkillsScreen (HermesScaffold handles it) Per AGENTS.md — HermesScaffold already offsets for the top bar internally, so .padding(padding) on inner content creases double-stacked top padding. * refactor: merge preview + edit button in SkillCard into single adaptive action Per AGENTS.md — two nearly-identical icon buttons (Preview + Edit) side by side was confusing. Merged into one Edit button since both opened the same skill content dialog (editable for installed, read-only for hub is handled by the ViewModel). * fix: revert onCategoryChange to non-null String (FilterChipRow<T> needs it) Also removes leftover Preview icon import that ktlint flagged. * refactor: restructure SkillCard layout — name gets full width, actions below The old layout crammed icon, name, source badge, edit button, uninstall button, AND toggle switch all on one horizontal row. Long skill names got truncated to 2-3 characters. New layout: - Top row: icon | name (weight 1f) | source badge | toggle switch - Below: category (if present) - Below: description (if present) - Bottom row: edit + uninstall buttons (end-aligned, only if hub source) The name Text now has weight(1f) so it fills all available space. * fix: fix brace nesting + move icon to far left for extra name space * fix: add missing height import * refactor: remove Extension icon from SkillCard for cleaner layout * fix: remove double-stacked padding in SkillEditorDialog and SkillPreviewDialog Both dialogs had the AGENTS.md footgun: HermesScaffold already offsets the top bar internally, but the inner Box had .padding(padding) stacking an extra ~56dp gap between the header and the edit text field. * fix(skills-hub): unwrap SkillHubSearchResponse wrapper Backend changed /api/skills/hub/search from returning a raw JSON array to an object wrapper: { results: [...], source_counts: {...}, ... } - Added SkillHubSearchResponse, SkillHubInstalledEntry models - Extended HubSkill with identifier, trustLevel, repo, tags - Updated API endpoint return type to SkillHubSearchResponse - ViewModel unwraps data?.results.orEmpty() instead of data.orEmpty() * fix: unwrap SkillHubSearchResponse in ProfilesViewModel too + remove unused HubSkill import * fix: update test mocks to use SkillHubSearchResponse wrapper * fix: refactor HubSkillCard layout — remove icon, name gets full width, install below * fix: use unique key for hub results + weight(1f) to prevent scroll crash * fix(skills): resolve crash due to duplicate keys in lazy list * feat(skills): move search hub button into trailing icon of search bar --------- Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
1 parent
c33821e71c
commit
b46e0ef46d
8 files changed
+1260
-285
No files matched your search
@@ -1,15 +1,55 @@
|
||||
package com.m57.hermescontrol.data.model
|
||||
|
||||
import com.google.gson.annotations.SerializedName
|
||||
|
||||
data class Skill(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val category: String?,
|
||||
val enabled: Boolean,
|
||||
val content: String? = null,
|
||||
val source: String? = null, // built-in, hub, optional
|
||||
val pinned: Boolean? = null,
|
||||
val linked_files: List<String>? = null,
|
||||
)
|
||||
|
||||
data class HubSkill(
|
||||
val name: String,
|
||||
val description: String?,
|
||||
val source: String?,
|
||||
val category: String? = null,
|
||||
val identifier: String? = null,
|
||||
@SerializedName("trust_level") val trustLevel: String? = null,
|
||||
val repo: String? = null,
|
||||
val tags: List<String>? = null,
|
||||
)
|
||||
|
||||
data class SkillHubSearchResponse(
|
||||
val results: List<HubSkill>,
|
||||
@SerializedName("source_counts") val sourceCounts: Map<String, Int>? = null,
|
||||
@SerializedName("timed_out") val timedOut: List<String>? = null,
|
||||
val installed: Map<String, SkillHubInstalledEntry>? = null,
|
||||
)
|
||||
|
||||
data class SkillHubInstalledEntry(
|
||||
val name: String? = null,
|
||||
@SerializedName("trust_level") val trustLevel: String? = null,
|
||||
@SerializedName("scan_verdict") val scanVerdict: String? = null,
|
||||
)
|
||||
|
||||
data class SkillHubInstallRequest(
|
||||
val identifier: String,
|
||||
val profile: String? = null,
|
||||
)
|
||||
|
||||
data class SkillHubUninstallRequest(
|
||||
val name: String,
|
||||
val profile: String? = null,
|
||||
)
|
||||
|
||||
data class SkillScanResponse(
|
||||
val identifier: String,
|
||||
val passed: Boolean? = null,
|
||||
val issues: List<String>? = null,
|
||||
val warnings: List<String>? = null,
|
||||
)
|
||||
@@ -29,7 +29,6 @@ import com.m57.hermescontrol.data.model.EnvVarRevealRequest
|
||||
import com.m57.hermescontrol.data.model.EnvVarRevealResponse
|
||||
import com.m57.hermescontrol.data.model.EnvVarUpdate
|
||||
import com.m57.hermescontrol.data.model.HookResponse
|
||||
import com.m57.hermescontrol.data.model.HubSkill
|
||||
import com.m57.hermescontrol.data.model.KanbanBoardResponse
|
||||
import com.m57.hermescontrol.data.model.KanbanBoardsResponse
|
||||
import com.m57.hermescontrol.data.model.KanbanTask
|
||||
@@ -68,6 +67,10 @@ import com.m57.hermescontrol.data.model.SessionStatsResponse
|
||||
import com.m57.hermescontrol.data.model.SetActiveProfileRequest
|
||||
import com.m57.hermescontrol.data.model.Skill
|
||||
import com.m57.hermescontrol.data.model.SkillContentResponse
|
||||
import com.m57.hermescontrol.data.model.SkillHubInstallRequest
|
||||
import com.m57.hermescontrol.data.model.SkillHubSearchResponse
|
||||
import com.m57.hermescontrol.data.model.SkillHubUninstallRequest
|
||||
import com.m57.hermescontrol.data.model.SkillScanResponse
|
||||
import com.m57.hermescontrol.data.model.StatusResponse
|
||||
import com.m57.hermescontrol.data.model.SystemStatsResponse
|
||||
import com.m57.hermescontrol.data.model.ToggleSkillRequest
|
||||
@@ -156,10 +159,35 @@ interface HermesApiService {
|
||||
@GET("api/skills")
|
||||
suspend fun getSkills(): Response<List<Skill>>
|
||||
|
||||
@POST("api/skills/hub/search")
|
||||
@GET("api/skills/hub/search")
|
||||
suspend fun searchSkillsHub(
|
||||
@Query("q") q: String,
|
||||
): Response<List<HubSkill>>
|
||||
@Query("source") source: String? = null,
|
||||
@Query("limit") limit: Int? = null,
|
||||
): Response<SkillHubSearchResponse>
|
||||
|
||||
@GET("api/skills/hub/preview")
|
||||
suspend fun previewHubSkill(
|
||||
@Query("identifier") identifier: String,
|
||||
): Response<SkillContentResponse>
|
||||
|
||||
@GET("api/skills/hub/scan")
|
||||
suspend fun scanHubSkill(
|
||||
@Query("identifier") identifier: String,
|
||||
): Response<SkillScanResponse>
|
||||
|
||||
@GET("api/skills/hub/sources")
|
||||
suspend fun getHubSources(): Response<List<String>>
|
||||
|
||||
@POST("api/skills/hub/install")
|
||||
suspend fun installHubSkill(
|
||||
@Body body: SkillHubInstallRequest,
|
||||
): Response<ActionResponse>
|
||||
|
||||
@POST("api/skills/hub/uninstall")
|
||||
suspend fun uninstallHubSkill(
|
||||
@Body body: SkillHubUninstallRequest,
|
||||
): Response<ActionResponse>
|
||||
|
||||
@PUT("api/skills/toggle")
|
||||
suspend fun toggleSkill(
|
||||
|
||||
@@ -4,6 +4,7 @@ import androidx.compose.foundation.background
|
||||
import androidx.compose.foundation.layout.Arrangement
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.PaddingValues
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.padding
|
||||
@@ -230,35 +231,28 @@ fun SearchBar(
|
||||
// ── FilterChipRow — horizontally scrollable filter chips ───────────────
|
||||
|
||||
@Composable
|
||||
fun FilterChipRow(
|
||||
chips: List<String>,
|
||||
selectedChip: String?,
|
||||
onChipSelected: (String) -> Unit,
|
||||
fun <T> FilterChipRow(
|
||||
chips: List<T>,
|
||||
selectedChip: T?,
|
||||
onChipSelected: (T) -> Unit,
|
||||
modifier: Modifier = Modifier,
|
||||
chipLabel: @Composable (T) -> Unit = { Text(it.toString()) },
|
||||
) {
|
||||
val spacing = LocalSpacing.current
|
||||
LazyRow(
|
||||
modifier = modifier.fillMaxWidth(),
|
||||
contentPadding =
|
||||
androidx.compose.foundation.layout.PaddingValues(
|
||||
PaddingValues(
|
||||
horizontal = spacing.md,
|
||||
vertical = spacing.xs,
|
||||
),
|
||||
horizontalArrangement = Arrangement.spacedBy(spacing.sm),
|
||||
) {
|
||||
items(chips, key = { it }) { chip ->
|
||||
items(chips, key = { it.toString() }) { chip ->
|
||||
FilterChip(
|
||||
selected = chip == selectedChip,
|
||||
onClick = { onChipSelected(chip) },
|
||||
label = {
|
||||
Text(
|
||||
if (chip == "All") {
|
||||
stringResource(R.string.skills_category_all)
|
||||
} else {
|
||||
chip
|
||||
},
|
||||
)
|
||||
},
|
||||
label = { chipLabel(chip) },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,7 +332,7 @@ class ProfilesViewModel :
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isSearchingHub = false,
|
||||
hubSearchResults = result.data,
|
||||
hubSearchResults = result.data?.results.orEmpty(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -1,8 +1,14 @@
|
||||
package com.m57.hermescontrol.ui.skills
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.m57.hermescontrol.R
|
||||
import com.m57.hermescontrol.data.model.HubSkill
|
||||
import com.m57.hermescontrol.data.model.SaveSkillContentRequest
|
||||
import com.m57.hermescontrol.data.model.Skill
|
||||
import com.m57.hermescontrol.data.model.SkillHubInstallRequest
|
||||
import com.m57.hermescontrol.data.model.SkillHubUninstallRequest
|
||||
import com.m57.hermescontrol.data.model.ToggleSkillRequest
|
||||
import com.m57.hermescontrol.data.remote.ApiClient
|
||||
import com.m57.hermescontrol.data.remote.NetworkResult
|
||||
@@ -18,6 +24,21 @@ import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
enum class SkillsViewMode {
|
||||
INSTALLED,
|
||||
HUB,
|
||||
}
|
||||
|
||||
enum class SkillFilter(val labelRes: Int) {
|
||||
ALL_STATUSES(R.string.skills_status_all),
|
||||
ENABLED(R.string.skills_status_enabled),
|
||||
DISABLED(R.string.skills_status_disabled),
|
||||
}
|
||||
|
||||
enum class CategoryFilter {
|
||||
ALL,
|
||||
}
|
||||
|
||||
data class SkillsUiState(
|
||||
val isLoading: Boolean = false,
|
||||
val skills: List<Skill> = emptyList(),
|
||||
@@ -28,15 +49,34 @@ data class SkillsUiState(
|
||||
val skillContent: String? = null,
|
||||
val isSavingContent: Boolean = false,
|
||||
val saveContentSuccess: Boolean = false,
|
||||
// View mode
|
||||
val viewMode: SkillsViewMode = SkillsViewMode.INSTALLED,
|
||||
// Hub search
|
||||
val hubQuery: String = "",
|
||||
val hubResults: List<HubSkill> = emptyList(),
|
||||
val isHubSearching: Boolean = false,
|
||||
val hubSearchError: String? = null,
|
||||
// Hub install
|
||||
val isInstalling: Boolean = false,
|
||||
val installingSkillName: String? = null,
|
||||
val isUninstalling: Boolean = false,
|
||||
val uninstallingSkillName: String? = null,
|
||||
// Preview
|
||||
val previewSkillName: String? = null,
|
||||
val previewSkillContent: String? = null,
|
||||
val isLoadingPreview: Boolean = false,
|
||||
// Source filter for installed tab
|
||||
val sourceFilter: String? = null,
|
||||
)
|
||||
|
||||
class SkillsViewModel :
|
||||
ViewModel(),
|
||||
class SkillsViewModel(application: Application) :
|
||||
AndroidViewModel(application),
|
||||
ToastHost {
|
||||
private val _uiState = MutableStateFlow(SkillsUiState())
|
||||
val uiState: StateFlow<SkillsUiState> = _uiState.asStateFlow()
|
||||
|
||||
private var loadJob: Job? = null
|
||||
private var searchJob: Job? = null
|
||||
|
||||
fun loadSkills() {
|
||||
loadJob =
|
||||
@@ -58,11 +98,188 @@ class SkillsViewModel :
|
||||
)
|
||||
}
|
||||
|
||||
fun setViewMode(mode: SkillsViewMode) {
|
||||
_uiState.update { it.copy(viewMode = mode) }
|
||||
if (mode == SkillsViewMode.INSTALLED && _uiState.value.skills.isEmpty()) {
|
||||
loadSkills()
|
||||
}
|
||||
}
|
||||
|
||||
fun setSourceFilter(source: String?) {
|
||||
_uiState.update { it.copy(sourceFilter = source) }
|
||||
}
|
||||
|
||||
fun setHubQuery(query: String) {
|
||||
_uiState.update { it.copy(hubQuery = query) }
|
||||
}
|
||||
|
||||
// ── Hub search ──────────────────────────────────────────────────────────
|
||||
|
||||
fun searchHub(query: String) {
|
||||
_uiState.update { it.copy(hubQuery = query, hubSearchError = null) }
|
||||
searchJob?.cancel()
|
||||
if (query.isBlank()) {
|
||||
_uiState.update { it.copy(hubResults = emptyList(), isHubSearching = false) }
|
||||
return
|
||||
}
|
||||
searchJob =
|
||||
safeLaunchLoad(
|
||||
currentJob = searchJob,
|
||||
apiCall = {
|
||||
safeApiCall { ApiClient.hermesApi.searchSkillsHub(q = query) }
|
||||
},
|
||||
onStart = { _uiState.update { it.copy(isHubSearching = true) } },
|
||||
onSuccess = { data ->
|
||||
_uiState.update { it.copy(isHubSearching = false, hubResults = data?.results.orEmpty()) }
|
||||
},
|
||||
onError = { errorMsg ->
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isHubSearching = false,
|
||||
hubSearchError = "Search failed: $errorMsg",
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fun clearHubSearch() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
hubQuery = "",
|
||||
hubResults = emptyList(),
|
||||
hubSearchError = null,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Hub install ─────────────────────────────────────────────────────────
|
||||
|
||||
fun installSkill(identifier: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isInstalling = true,
|
||||
installingSkillName = identifier,
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.installHubSkill(SkillHubInstallRequest(identifier = identifier)) }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isInstalling = false,
|
||||
installingSkillName = null,
|
||||
toastMessage = "Installed: $identifier",
|
||||
)
|
||||
}
|
||||
// Refresh installed skills
|
||||
loadSkills()
|
||||
}
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isInstalling = false,
|
||||
installingSkillName = null,
|
||||
toastMessage = "Failed to install $identifier: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallSkill(name: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isUninstalling = true,
|
||||
uninstallingSkillName = name,
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.uninstallHubSkill(SkillHubUninstallRequest(name = name)) }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isUninstalling = false,
|
||||
uninstallingSkillName = null,
|
||||
toastMessage = "Uninstalled: $name",
|
||||
)
|
||||
}
|
||||
loadSkills()
|
||||
}
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isUninstalling = false,
|
||||
uninstallingSkillName = null,
|
||||
toastMessage = "Failed to uninstall $name: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── SKILL.md preview ────────────────────────────────────────────────────
|
||||
|
||||
fun previewSkill(skillName: String) {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
previewSkillName = skillName,
|
||||
previewSkillContent = null,
|
||||
isLoadingPreview = true,
|
||||
)
|
||||
}
|
||||
viewModelScope.launch {
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.getSkillContent(skillName) }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingPreview = false,
|
||||
previewSkillContent = result.data?.content,
|
||||
)
|
||||
}
|
||||
}
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
isLoadingPreview = false,
|
||||
toastMessage = "Failed to load preview: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearPreview() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
previewSkillName = null,
|
||||
previewSkillContent = null,
|
||||
isLoadingPreview = false,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// ── Existing functionality ──────────────────────────────────────────────
|
||||
|
||||
fun toggleSkill(skill: Skill) {
|
||||
val originalEnabled = skill.enabled
|
||||
val targetEnabled = !originalEnabled
|
||||
|
||||
// Optimistically update
|
||||
_uiState.update { state ->
|
||||
state.copy(
|
||||
skills =
|
||||
@@ -78,7 +295,11 @@ class SkillsViewModel :
|
||||
safeApiCall { ApiClient.hermesApi.toggleSkill(ToggleSkillRequest(skill.name, targetEnabled)) }
|
||||
}
|
||||
if (result is NetworkResult.Failure) {
|
||||
revertSkillToggle(skill.name, originalEnabled, "Failed to toggle skill: ${result.error.message}")
|
||||
revertSkillToggle(
|
||||
skill.name,
|
||||
originalEnabled,
|
||||
"Failed to toggle skill: ${result.error.message}",
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -146,11 +367,10 @@ class SkillsViewModel :
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall {
|
||||
ApiClient.hermesApi.saveSkillContent(
|
||||
com.m57.hermescontrol.data.model
|
||||
.SaveSkillContentRequest(
|
||||
name = skillName,
|
||||
content = content,
|
||||
),
|
||||
SaveSkillContentRequest(
|
||||
name = skillName,
|
||||
content = content,
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -178,6 +398,25 @@ class SkillsViewModel :
|
||||
}
|
||||
}
|
||||
|
||||
fun updateSkillsFromHub() {
|
||||
viewModelScope.launch {
|
||||
_uiState.update { it.copy(toastMessage = "Updating skills from hub…") }
|
||||
val result =
|
||||
withContext(Dispatchers.IO) {
|
||||
safeApiCall { ApiClient.hermesApi.updateSkillsFromHub() }
|
||||
}
|
||||
when (result) {
|
||||
is NetworkResult.Success -> {
|
||||
_uiState.update { it.copy(toastMessage = "Skills updated") }
|
||||
loadSkills()
|
||||
}
|
||||
is NetworkResult.Failure -> {
|
||||
_uiState.update { it.copy(toastMessage = "Update failed: ${result.error.message}") }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun clearEditor() {
|
||||
_uiState.update {
|
||||
it.copy(
|
||||
|
||||
Reference in new issue
Block a user