feat(#446): plugins tab full parity — install from URL, provider selection, remove confirm, badges (#456)

* docs: document HermesScaffold padding foot-gun root cause in AGENTS.md

* feat(#446): plugins tab full parity — install from URL, provider selection, remove confirm, badges

- Add Install from URL section with force reinstall + enable after install toggles
- Add Plugin Provider selection section (memory provider + context engine dropdowns)
- Add rescan button in top bar actions
- Add confirmation dialog before removing plugins
- Show source badges, runtime status, auth badges on plugin cards
- Show dashboard slots from plugin manifest
- Add visibility toggle (show/hide in sidebar) for manifest plugins
- Show orphan dashboard plugins section
- Add PluginManifestData, PluginsHubProviders, ProviderOption models
- Add rescanPlugins, savePluginProviders, setPluginVisibility API endpoints

* fix(#446): fix CI compilation errors — DashboardPluginMeta mapping and @Composable context

* fix(#446): pass missing PluginInfo ctor params for orphan plugin mapping

* fix(#446): remove duplicate refresh button in plugins top bar

Scaffold's onRefresh param added a second refresh icon alongside the
rescan action button. Removed onRefresh/isRefreshing from HermesScaffold
and moved pull-to-refresh into a manual PullToRefreshBox wrapping the
content, keeping the rescan icon as the sole top-bar action.

* fix(#446): remove rescan icon from plugins top bar

Only one action icon left in top bar; pull-to-refresh (loadPlugins)
remains the sole refresh mechanism. rescanPlugins() viewmodel logic
is unchanged but now unreachable from the UI.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-07-03 15:31:43 +03:00
1 parent 0ee24144c0
commit 57a815c67e
6 files changed
+881 -109

No files matched your search

@@ -9,6 +9,7 @@ data class PluginInfo(
val source: String?,
@SerializedName("runtime_status") val runtimeStatus: String?,
@SerializedName("has_dashboard_manifest") val hasDashboardManifest: Boolean = false,
@SerializedName("dashboard_manifest") val dashboardManifest: PluginManifestData? = null,
@SerializedName("can_remove") val canRemove: Boolean = false,
@SerializedName("can_update_git") val canUpdateGit: Boolean = false,
@SerializedName("auth_required") val authRequired: Boolean = false,
@@ -24,8 +25,50 @@ data class PluginInfo(
runtimeStatus.equals("disabled", ignoreCase = true)
}
data class PluginManifestData(
val name: String? = null,
val label: String? = null,
val description: String? = null,
val icon: String? = null,
val version: String? = null,
val tab: PluginTabInfo? = null,
val slots: List<String>? = null,
val entry: String? = null,
val css: String? = null,
@SerializedName("has_api") val hasApi: Boolean = false,
val source: String? = null,
)
data class PluginTabInfo(
val path: String? = null,
val position: String? = null,
val override: String? = null,
val hidden: Boolean = false,
)
data class DashboardPluginMeta(
val name: String? = null,
val label: String? = null,
val description: String? = null,
val tab: PluginTabInfo? = null,
)
data class PluginsHubProviders(
@SerializedName("memory_provider") val memoryProvider: String? = null,
@SerializedName("memory_options") val memoryOptions: List<ProviderOption> = emptyList(),
@SerializedName("context_engine") val contextEngine: String? = null,
@SerializedName("context_options") val contextOptions: List<ProviderOption> = emptyList(),
)
data class ProviderOption(
val name: String,
val description: String? = null,
)
data class PluginsHubResponse(
val plugins: List<PluginInfo>,
@SerializedName("orphan_dashboard_plugins") val orphanDashboardPlugins: List<DashboardPluginMeta> = emptyList(),
val providers: PluginsHubProviders? = null,
)
data class AgentPluginInstallBody(
@@ -33,3 +76,8 @@ data class AgentPluginInstallBody(
val force: Boolean = false,
val enable: Boolean = true,
)
data class PluginProvidersPutRequest(
@SerializedName("memory_provider") val memoryProvider: String? = null,
@SerializedName("context_engine") val contextEngine: String? = null,
)
@@ -29,6 +29,7 @@ import com.m57.hermescontrol.data.model.ModelOptionsResponse
import com.m57.hermescontrol.data.model.PairingApproveRequest
import com.m57.hermescontrol.data.model.PairingResponse
import com.m57.hermescontrol.data.model.PairingRevokeRequest
import com.m57.hermescontrol.data.model.PluginProvidersPutRequest
import com.m57.hermescontrol.data.model.PluginsHubResponse
import com.m57.hermescontrol.data.model.ProfileSoulResponse
import com.m57.hermescontrol.data.model.ProfilesResponse
@@ -319,6 +320,20 @@ interface HermesApiService {
@Path("name", encoded = true) name: String,
): Response<Unit>
@POST("api/dashboard/plugins/rescan")
suspend fun rescanPlugins(): Response<Unit>
@PUT("api/dashboard/plugin-providers")
suspend fun savePluginProviders(
@Body body: PluginProvidersPutRequest,
): Response<Unit>
@POST("api/dashboard/plugins/{name}/visibility")
suspend fun setPluginVisibility(
@Path("name", encoded = true) name: String,
@Body body: Map<String, Boolean>,
): Response<Unit>
@GET("api/messaging/platforms")
suspend fun getMessagingPlatforms(
@Query("profile") profile: String? = null,