editor: fix scrambled timeline clip order after global-prompt edits

The cut order (timeline_order) is a list of scene ids. Editing the global
prompt rebuilds the plan and reassigns scene content to ids positionally, but
_distributeGlobalPrompt left timeline_order untouched — so a stale id sequence
scrambled the live clips against their plan badges (e.g. 01,02,04,03,...),
even though ghost re-layout (plan-anchored) stayed correct.

Fix: the cut order tracks the plan until the user explicitly pins a custom
order via the ◀ ▶ arrows. New Project.timeline_manually_ordered flag, set by
moveTimelineClip; _ensureTimelineOrder follows plan order when it's unset and
heals (survivors keep their manual order, new appended) when set. Reconcile on
global-prompt apply too. Existing drifted projects self-heal on load.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
digital-garbage
2026-06-23 17:15:55 +03:00
co-authored by Claude Opus 4.8
parent dc70e2b9c6
commit c5ead80ae8
2 changed files with 15 additions and 0 deletions
+5
View File
@@ -597,6 +597,10 @@ class Project:
# Cut order: scene ids in TIMELINE (result) order, independent of plan (scenes) order.
# Empty = follow plan order (back-compat). Reordering the plan never touches this.
timeline_order: list = field(default_factory=list)
# True once the user explicitly reordered a timeline clip (◀ ▶). Until then the cut order
# tracks the plan, so editing the global prompt re-derives it cleanly instead of letting a
# stale id sequence scramble the clips against the plan badges.
timeline_manually_ordered: bool = False
created_at: float = field(default_factory=time.time)
updated_at: float = field(default_factory=time.time)
@@ -634,6 +638,7 @@ class Project:
scene_renders=dict(d.get("scene_renders") or {}),
scene_ghosts=list(d.get("scene_ghosts") or []),
timeline_order=list(d.get("timeline_order") or []),
timeline_manually_ordered=bool(d.get("timeline_manually_ordered", False)),
created_at=float(d.get("created_at", time.time())),
updated_at=float(d.get("updated_at", time.time())),
)
+10
View File
@@ -488,6 +488,9 @@
_applyDetectedTransitions(next, v.transitions, text);
state.project.scenes = next;
state.sceneGhosts = ghostsOut;
// The global prompt is authoritative on plan order — reconcile the cut order to it now
// (no-op when the user has pinned a manual order; otherwise tracks the plan).
_ensureTimelineOrder();
const prevSel = state.selectedSceneId;
const stillSel = prevSel && next.some((s) => s.id === prevSel);
const firstId = stillSel ? prevSel : (next[0]?.id || null);
@@ -1422,6 +1425,10 @@
function _ensureTimelineOrder() {
const p = state.project; if (!p) return;
const ids = (p.scenes || []).map((s) => s.id);
// Until the user explicitly pins a custom cut order (◀ ▶), the timeline follows the plan.
// This keeps an edit to the global prompt — which rebuilds the plan in its own order — from
// leaving a stale id sequence behind that scrambles the clips against their plan badges.
if (!p.timeline_manually_ordered) { p.timeline_order = ids; return; }
const idSet = new Set(ids);
const cur = (Array.isArray(p.timeline_order) ? p.timeline_order : []).filter((id) => idSet.has(id));
const inCur = new Set(cur);
@@ -1450,6 +1457,9 @@
if (i === j) return;
_historyRecord();
order.splice(i, 1); order.splice(j, 0, id);
// The user has now pinned a custom cut order — a global-prompt edit must preserve it
// rather than re-deriving the order from the plan.
p.timeline_manually_ordered = true;
notify(); scheduleSave();
}