fix(design-view): adopt pre-existing track mode on drop to prevent lane strand

A bank-panel capture dropped onto a track with pre-existing content was
auto-tagged into the active mode, forcing a multi-mode lane split whose
toggle silenced the pre-existing items. New items now adopt the single
mode of their track's prior content; deliberate splits stay on the
explicit item-move path.
This commit is contained in:
2026-07-27 04:50:46 -04:00
parent e0d4358452
commit 8887995d7a
4 changed files with 238 additions and 10 deletions
+30 -5
View File
@@ -493,11 +493,35 @@ TrackPlan makeRestorePlan(const std::string& guid, const TrackSnapshot& snap);
// such an item `onManualLane = true` (it knows the item's lane and consults the
// ownership index); the decision then emits NO tag for it. New tracks and new items on
// managed/no lane follow the active-mode rule.
//
// -- Pre-existing-content adoption (strand fix) -------------------------------
//
// A new item dropped onto a track that ALREADY carries currently-visible content must
// not silently push that track into a different mode. If the pre-existing content
// resolves to ONE mode and the new item were blindly tagged to the (different) ACTIVE
// mode, the track would become multi-mode, planLaneMinting would split it, and the
// toggle would silence whichever lane the active mode does not own — stranding the
// pre-existing, previously-visible items on a C_LANEPLAYS=0 lane with no user intent.
//
// The rule: a new item ADOPTS the single mode of the pre-existing content already on its
// track. Only when the track carries no pre-existing managed-eligible content (an empty
// or brand-new track), or when that content already spans multiple modes (an existing
// deliberate split, which the new item joins under the active mode), does the new item
// fall back to the active-mode rule. Deliberate two-take splits are unaffected: those go
// through the explicit item mode-move actions (planItemRetag), never auto-tag.
// The shell reports each new item's track pre-existing-content modes in `trackModes`.
// One new item the shell detected this poll. Its lane disposition decides exemption.
// One new item the shell detected this poll. Its lane disposition decides exemption; its
// track's pre-existing content modes decide adoption (see above).
struct NewItem {
std::string guid;
bool onManualLane = false; // true ⇒ EXEMPT from auto-tag (hand-managed lane)
// The distinct modes the PRE-EXISTING (not-new-this-tick) managed-eligible content on
// this item's track resolves to. Empty ⇒ the item's track carried no prior content, so
// the item takes the active mode. Exactly one ⇒ ADOPT that mode (the strand guard).
// More than one ⇒ the track is already a deliberate split; the item takes the active
// mode. The shell fills this by resolving each pre-existing item's mode from membership.
std::set<std::string> trackModes;
};
// One membership write the auto-tag decision produced: tag `guid` into `modeId`. The
@@ -511,10 +535,11 @@ struct AutoTag {
// The pure auto-tag decision: given the new track GUIDs and new items detected this
// poll plus the active mode, produce the membership writes. Every new track is tagged
// to `activeMode`; every new item is tagged to `activeMode` UNLESS it landed on a
// manual lane (exempt). An empty `activeMode` yields no tags (nothing to tag into).
// Empty GUIDs are skipped. The result is a plan the shell applies; this function
// mutates nothing.
// to `activeMode`. Every new item is tagged UNLESS it landed on a manual lane (exempt);
// its target mode is the single mode of its track's pre-existing content (adoption — the
// strand guard) when that content resolves to exactly one mode, otherwise `activeMode`.
// An empty `activeMode` yields no tags (nothing to tag into). Empty GUIDs are skipped.
// The result is a plan the shell applies; this function mutates nothing.
std::vector<AutoTag> autoTagNewContent(const std::vector<std::string>& newTrackGuids,
const std::vector<NewItem>& newItems,
const std::string& activeMode);