* feat(desktop): per-profile remote gateway hosts
Profile switching silently failed whenever the desktop was connected to a
remote backend: the rail routed non-active profiles to a local pool backend,
but spawnPoolBackend hard-threw "Profiles are unavailable when connected to a
remote Hermes backend", and the renderer swallowed the error into an infinite
reconnect backoff while still marking the profile active. Remote was also a
single app-global setting, so there was no way to give a profile its own host.
Add per-profile remote hosts so each profile can point at its own backend:
- connection.json gains a validated `profiles` map; profileRemoteOverride()
(pure, unit-tested) selects an explicit per-profile remote.
- resolveRemoteBackend(profile) precedence: per-profile override → env override
→ global remote → local spawn. spawnPoolBackend now connects to a profile's
remote (no local child) instead of throwing; startHermes resolves the primary
profile's remote.
- coerce/sanitize connection config are scope-aware (global vs named profile)
and preserve each other's entries; IPC get/save/apply/test thread an optional
profile. Per-profile apply drops only that profile's pool backend.
- Settings → Gateway adds an "Applies to" scope selector reusing the existing
URL/token/OAuth/test UX per profile.
Tests: connection-config pure suite (+6) and desktop platform suite pass;
tsc/eslint/vitest clean.
* refactor(desktop): DRY per-profile remote helpers
Share connectionScopeKey + normAuthMode from connection-config.cjs (drop the
main.cjs copy), collapse the scope/auth ternaries, route the env remote through
buildRemoteConnection, and fold the duplicated remote-block validation into
buildRemoteBlock. No behavior change; pure suite + live E2E still green.