The right rail's tabs were persisted under ONE global localStorage key, so a
preview opened in one agent's chat appeared in every other agent's chat —
Tess's 3D model showed up in VEXA's rail and vice versa.
apps/desktop/AGENTS.md states the rule this violates: "Persisted state must
declare its scope in its own key: is this global, or does it belong to a
connection, a profile, a stored session, a project, or a window? Getting the
scope wrong is how one profile's setting bleeds into another."
The scope is the CHAT ON SCREEN, deliberately not the window's gateway socket:
a focused tab does not swap the socket, and every Bot Mode chat is served by one
pooled backend, so a socket-keyed rail shows one agent's previews in every
agent's chat. bot-row.tsx documents the same trap for the roster highlight
("Highlight follows the chat on screen (focused session's owner), not the
gateway socket's home — a focused tab doesn't swap the socket").
The rail therefore resolves its scope from the focused session's owner via the
existing knownOwnerForSession ladder, the same value the roster highlight
trusts, so the two cannot disagree about whose chat is on screen.
session-states.ts owns that resolver and already imports preview.ts, so it
pushes the scope in (setPreviewScope) rather than the rail reaching back, which
would be an import cycle.
The bucket map mirrors tilesByProfile, including a "view key" that follows a
rename (without it the persist subscriber re-creates the bucket the rename just
deleted) and lazy adoption of a legacy global array into the first scope that
arrives, so tabs the user can see survive the migration. Registered in both
dropTilesForProfile and migrateTilesForProfile, as the guide requires of any
profile-keyed localStorage family.
Also gives each non-primary profile its own Electron session partition on the
session pop-out and instance windows, so a per-agent window keeps its own rail
even across partitions.
Tests: preview-tabs-scope.test.ts holds the contract (no cross-agent bleed, per
agent buckets, rename moves rather than strands, and a socket change does NOT
re-home the rail — the regression that made the first cut look correct).
(cherry picked from commit dd40bd75fe7e27b9164af67b352884fc28242fef)