Editor's undo/redo doesn't truncate the redo stack when a new edit is made after undoing — user can re-apply the 'undone' action by pressing Redo, even though the model says it should be discarded #237
Labels
No labels
blocked:commit-ready-failed
blocked:gate-fetch
blocked:gate-lint
blocked:gate-plan
blocked:gate-research
blocked:needs-attention
blocked:needs-decision
blocked:needs-human
blocked:no-changes
blocked:verify-failed
bug
deferred
feature-request
in-progress
needs-decompose
needs-triage
quickfix
security/medium
triage-approved
No milestone
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set.
Reference
Mark/Kimi_Agent_Brother_10_Digitizing_App#237
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Finding
src/pages/Editor.tsx:463pushHistorydoes:The
setHistoryadds the new snapshot to the end of the array. But it does NOT truncate the redo stack. The standard undo/redo model is:history = [...edits, currentEdit]where the array ends at the current editThe current implementation just appends the new edit and bumps the index. So the redo stack is preserved.
Repro
OK, the actual sequence depends on what
handleUndodoes. Let me re-trace.After step 3: history = [s0, s1, s2], historyIndex = 1, paths = s1.
After step 4: handleNodeDrag or similar is called. It calls
pushHistory(currentPaths).currentPathsiss1(the state from step 3). So the new entry in history is... a duplicate of s1? Or a forward move from s1 to a new state s3?Actually, the model is "history stores snapshots" and the snapshot is the state BEFORE the change. So:
pathsat the time of the push, which IS s1.So the user undid edit 2, then made edit 3, and now can redo "back" to s2. But s2 is the edit 2 state — which they thought they undid. The redo brings them to a state they thought was discarded.
This is the standard undo/redo bug when the redo stack isn't cleared.
Where
pushHistory:src/pages/Editor.tsx:463-475setHistory((prev) => [...prev, deepSnapshot]))Suggested fix shape
In
pushHistory, truncate the redo stack before appending:historyIndexRefis a ref that mirrorshistoryIndex(so the callback can read the freshest value without a dependency cycle):The fix: when pushHistory is called after some undos, drop everything after the current
historyIndexbefore appending the new entry. This way the redo stack is gone and the user can't re-apply the "undone" action.Severity
Medium. The bug is subtle but affects the user's mental model of undo/redo. Power users notice.
Discovered by
Code review (2026-10-02) of editor undo/redo flow.
Related
Fixed in commit
65fff8a(origin/main).pushHistory now slices prev to historyIndexRef.current + 1 before appending the new snapshot, so a new edit after undoing drops the redo stack — matching the standard undo/redo model used by Figma / Illustrator / Inkscape.
A historyIndexRef mirrors historyIndex so the stable useCallback can read the freshest value without taking historyIndex into its dep array (which would re-create the callback on every edit and cascade through every handler depending on it).
Regression test added to src/pages/Editor.test.tsx — "new edit after undo discards redo stack (#237)". Verified it fails without the fix and passes with it. Full suite: 1200/1200 tests pass, tsc clean, lint clean.