feat(toolsets): per-toolset config/env/provider/post-setup management (#782) (#831)

* feat(toolsets): per-toolset config/env/provider/post-setup management (#782)

* fix(toolsets): per-entry toolset name for shared detail ViewModel (#782)

NavDisplay uses the activity-level ViewModelStoreOwner, so class-keyed
viewModel{} returns the same ToolsetDetailViewModel for every detail
entry; the constructor-captured name froze the first-opened toolset into
all entries (on-device: every toolset showed web's providers, no further
/config calls after the first). The screen now drives setToolset(name) +
loadConfig() per entry, and setToolset resets per-toolset state
(config, revealed secrets, running post-setup) so nothing bleeds across
toolsets.

* style(toolsets): single-line scrollable tool chip strip (#782)

A wrapping FlowRow stacked chips vertically for toolsets with many
tools (web etc.), ballooning card height — especially on large font
scales. Chips now sit on one horizontally scrollable row.

* chore(toolsets): drop stray terminal-noise line from chip strip change

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-06 17:57:58 +03:00
1 parent d9f437939a
commit fe79ff42b4
9 files changed
+1558 -16

No files matched your search

@@ -0,0 +1,95 @@
package com.m57.hermescontrol.data.model
import kotlinx.serialization.Serializable
/**
* Per-toolset management payloads — issue #782.
*
* Contracts verified against hermes-agent hermes_cli/web_routers/tools.py:
* - GET /api/tools/toolsets/{name}/config → provider matrix + key status
* (web toolset additionally resolves active_search_backend /
* active_extract_backend the way the runtime dispatchers do).
* - PUT /api/tools/toolsets/{name}/provider {provider} → {ok, name, provider,
* needs_nous_auth?, feature?} (managed Nous rows without Portal entitlement
* write the keys but won't activate until sign-in).
* - PUT /api/tools/toolsets/{name}/env {env: {KEY: value}} → {ok, name,
* saved[], skipped[], is_set{}}; keys validated against the toolset
* category's env-var allowlist, blank values skipped ("leave unchanged").
* - POST /api/tools/toolsets/{name}/post-setup {key} → {ok, pid, name,
* key}; spawns `hermes tools post-setup <key>` and the client tails
* GET /api/actions/{name}/status (see ActionStatusResponse).
*/
@Serializable
data class ToolsetConfigResponse(
val name: String,
val hasCategory: Boolean = false,
val providers: List<ToolsetProvider> = emptyList(),
val activeProvider: String? = null,
val activeSearchBackend: String? = null,
val activeExtractBackend: String? = null,
)
@Serializable
data class ToolsetProvider(
val name: String,
val badge: String? = null,
val tag: String? = null,
val envVars: List<ToolsetEnvVar> = emptyList(),
val postSetup: String? = null,
val requiresNousAuth: Boolean = false,
val isActive: Boolean = false,
val status: String? = null,
)
@Serializable
data class ToolsetEnvVar(
val key: String,
val prompt: String? = null,
val url: String? = null,
val default: String? = null,
val isSet: Boolean = false,
)
@Serializable
data class ToolsetProviderSelectRequest(
val provider: String,
val profile: String? = null,
)
@Serializable
data class ToolsetProviderSelectResponse(
val ok: Boolean = false,
val name: String? = null,
val provider: String? = null,
val needsNousAuth: Boolean = false,
val feature: String? = null,
)
@Serializable
data class ToolsetEnvUpdateRequest(
val env: Map<String, String>,
val profile: String? = null,
)
@Serializable
data class ToolsetEnvUpdateResponse(
val ok: Boolean = false,
val name: String? = null,
val saved: List<String> = emptyList(),
val skipped: List<String> = emptyList(),
val isSet: Map<String, Boolean> = emptyMap(),
)
@Serializable
data class ToolsetPostSetupRequest(
val key: String,
val profile: String? = null,
)
@Serializable
data class ToolsetPostSetupResponse(
val ok: Boolean = false,
val pid: Int? = null,
val name: String? = null,
val key: String? = null,
)
@@ -107,6 +107,13 @@ import com.m57.hermescontrol.data.model.TelegramOnboardingStartResponse
import com.m57.hermescontrol.data.model.TelegramOnboardingStatusResponse
import com.m57.hermescontrol.data.model.ToggleSkillRequest
import com.m57.hermescontrol.data.model.Toolset
import com.m57.hermescontrol.data.model.ToolsetConfigResponse
import com.m57.hermescontrol.data.model.ToolsetEnvUpdateRequest
import com.m57.hermescontrol.data.model.ToolsetEnvUpdateResponse
import com.m57.hermescontrol.data.model.ToolsetPostSetupRequest
import com.m57.hermescontrol.data.model.ToolsetPostSetupResponse
import com.m57.hermescontrol.data.model.ToolsetProviderSelectRequest
import com.m57.hermescontrol.data.model.ToolsetProviderSelectResponse
import com.m57.hermescontrol.data.model.ToolsetToggleRequest
import com.m57.hermescontrol.data.model.UpdateCheckResponse
import com.m57.hermescontrol.data.model.UpdateCronJobRequest
@@ -407,6 +414,30 @@ interface HermesApiService {
@Body body: ToolsetToggleRequest,
): Response<Unit>
// ── Per-toolset management (issue #782) ────────────────────────────
@GET("api/tools/toolsets/{name}/config")
suspend fun getToolsetConfig(
@Path("name") name: String,
): Response<ToolsetConfigResponse>
@PUT("api/tools/toolsets/{name}/provider")
suspend fun selectToolsetProvider(
@Path("name") name: String,
@Body body: ToolsetProviderSelectRequest,
): Response<ToolsetProviderSelectResponse>
@PUT("api/tools/toolsets/{name}/env")
suspend fun saveToolsetEnv(
@Path("name") name: String,
@Body body: ToolsetEnvUpdateRequest,
): Response<ToolsetEnvUpdateResponse>
@POST("api/tools/toolsets/{name}/post-setup")
suspend fun runToolsetPostSetup(
@Path("name") name: String,
@Body body: ToolsetPostSetupRequest,
): Response<ToolsetPostSetupResponse>
@GET("api/plugins/hermes-achievements/achievements")
suspend fun getAchievements(): Response<AchievementsResponse>