feat(profiles): first-class profile switching — phase 0 baseline port (#820)

* feat(profiles): make profile switching actually scope the app (issue #781 follow-up)

* fix(profiles): inherit connection token when switching to same-dashboard profile (issue #781 follow-up)

* fix(chat): reset open session on profile switch + prefill login profile (desktop parity)

* fix(ws): inject profile into profile-scoped RPC params so chat follows the selected profile

* test(chat): de-poison SlashCommandDispatchRpcTest (drop static Dispatchers mock)

mockkStatic(Dispatchers::class) + a stubbed Dispatchers.IO bleeds JVM-wide and
breaks later classes in suite order (CI 2026-08-06: COMMAND_DISPATCH captures
cross-wired → ComparisonFailure on lower stack PRs). ChatViewModel gains an
injected ioDispatcher (38 sites); the slash test constructs the VM with the
test dispatcher so the IO stub is unnecessary.

---------

Co-authored-by: M57 <hy4ri@users.noreply.github.com>
This commit is contained in:
M57andM57 authored and GitHub committed 2026-08-06 15:06:28 +03:00
1 parent e56d4c9bfa
commit 75ac0c4790
14 files changed
+579 -56

No files matched your search

@@ -19,6 +19,7 @@ import com.m57.hermescontrol.data.model.UpdateProfileSoulRequest
import com.m57.hermescontrol.data.remote.ApiClient
import com.m57.hermescontrol.data.remote.NetworkResult
import com.m57.hermescontrol.data.remote.safeApiCall
import com.m57.hermescontrol.data.ws.HermesWsClient
import com.m57.hermescontrol.ui.common.ToastHost
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.async
@@ -111,6 +112,24 @@ class ProfilesViewModel :
}
when (result) {
is NetworkResult.Success -> {
// Persist the LOCAL selection so ProfileScopeInterceptor scopes
// every management REST call (?profile=) to the new profile,
// and re-home the WebSocket so the live gateway follows too
// (mirrors desktop's re-home-on-switch).
//
// Token inheritance: the app keys tokens per profile
// (token_<profileId>) because connections can point at
// different dashboards, but profiles listed on THIS screen
// are all served by the SAME connected dashboard — so a
// profile created/selected in-app inherits the current
// connection's token instead of forcing a re-login.
val currentToken = AuthManager.getToken()
if (currentToken != null && AuthManager.getProfileToken(name) == null) {
AuthManager.setProfileToken(name, currentToken)
}
AuthManager.setSelectedProfileId(name)
HermesWsClient.disconnect()
HermesWsClient.connect()
_uiState.update { it.copy(toastMessage = "Switched to profile $name") }
loadProfiles()
}