A cold profile switch pays the full pool-backend spawn — Python boot,
port announcement, readiness probe, token adoption — before the
profile's gateway can even open. Measured with the new CDP harness
(scripts/measure-profile-switch.mjs, same family as
profile-session-switch.mjs): click → WS open is ~2.5-2.9s on a cold
profile, ~3-3.6s to a settled sidebar; a warm profile settles in
~0.5-0.8s. The pointer entering a profile square telegraphs the switch
hundreds of ms before the click lands, so start the spawn then.
- store/profile: prewarmProfileBackend(name) — fires the existing
hermesDesktop.getConnection IPC, which is idempotent (ensureBackend
returns the pooled connectionPromise), so the real switch joins the
in-flight spawn instead of starting it. Skips the active gateway
profile, throttles per profile (60s) so drive-by hovers can't spam
spawn attempts, and swallows failures — error UX belongs to the real
switch. No new IPC surface; the pool's existing LRU cap + idle reaper
still bound resource use, and the LRU guard never evicts a
keepalive-fresh backend for a hover spawn.
- sidebar/use-profile-prewarm: pointerenter/pointerleave handlers with
a 120ms dwell so sweeping the pointer across the rail or a
mixed-profile session list doesn't spawn a backend per element
crossed.
- Wired at the three switch surfaces: rail ProfileSquare, the condensed
ProfileDropdown items (extracted ProfileDropdownItem so each row owns
its dwell timer), and SidebarSessionRow (covers cross-profile resumes
from the all-profiles view; same-profile rows no-op inside the guard).
Measured E2E over CDP: synthetic hover on a cold profile square spawns
its backend in the background; the subsequent click settles in ~519ms
vs ~3.0-3.6s unhovered — and any hover shorter than the spawn still
shaves its dwell off the click's wait.
Verification: apps/desktop `npx tsc --noEmit` clean; full
`npx vitest run` 212 files / 1777 passed (new prewarm guard/throttle
tests in store/profile.test.ts); eslint + prettier clean.