Editor has no MAX_HISTORY cap on its undo stack — workspace caps at 50 (#172) but the editor's undo/redo grows unbounded across a long session #196

Closed
opened 2026-10-03 23:50:14 +00:00 by Mark · 1 comment
Owner

Finding

src/components/workspace/useWorkspace.ts:198 defines MAX_HISTORY = 50 and enforces it on line 213: if (newHistory.length > MAX_HISTORY) newHistory.shift();. The workspace's undo/redo never grows past 50 entries.

src/pages/Editor.tsx (the editor's main page) has no such cap. grep -n 'MAX_HISTORY' src/pages/Editor.tsx returns 0 matches. The pushHistory callback at line 670+ appends to history indefinitely.

A user who opens a long-lived editor session and edits a design for hours can have a history array of 10,000+ snapshots. Each snapshot is a JSON.parse(JSON.stringify(paths)) deep copy — for a 30-path design with 12 nodes each, that's 360 nodes × ~200 bytes = 72KB per snapshot × 10,000 = 720MB of heap.

historyIndex is a single number, so navigation is fine. But memory growth is real.

Where

  • Workspace cap: src/components/workspace/useWorkspace.ts:198 (the reference implementation)
  • No editor cap: src/pages/Editor.tsx:670 (pushHistory appends without limit)
  • Test that would catch it: there isn't one; the editor has no test for unbounded history

Suggested fix shape

Apply the same pattern the workspace uses:

const MAX_HISTORY = 50;

const pushHistory = useCallback((snapshot: StitchPath[]) => {
  const deepSnapshot = JSON.parse(JSON.stringify(snapshot));
  setHistory((prev) => {
    const next = [...prev, deepSnapshot];
    if (next.length > MAX_HISTORY) next.shift();
    return next;
  });
  setHistoryIndex((prev) => prev + 1); // stays at "end" of trimmed stack
}, []);

Note: the workspace increments historyIndex only on the user explicitly pushing (not on undo/redo). The editor's pushHistory is called on every state-changing dispatch, so the cap should preserve the same "you can always redo" behavior — historyIndex stays at the end of the trimmed stack.

Severity

Medium. Memory growth is real; the user feels it after a long session. The workspace side already has this fix; the editor side is the gap.

Discovered by

Gap analysis (2026-10-02) of editor memory safety.

  • #172 (open) — Workspace's history stack has no MAX_HISTORY. The reporter was right about the workspace gap; the editor has the same gap.
## Finding `src/components/workspace/useWorkspace.ts:198` defines `MAX_HISTORY = 50` and enforces it on line 213: `if (newHistory.length > MAX_HISTORY) newHistory.shift();`. The workspace's undo/redo never grows past 50 entries. `src/pages/Editor.tsx` (the editor's main page) has no such cap. `grep -n 'MAX_HISTORY' src/pages/Editor.tsx` returns 0 matches. The `pushHistory` callback at line 670+ appends to `history` indefinitely. A user who opens a long-lived editor session and edits a design for hours can have a `history` array of 10,000+ snapshots. Each snapshot is a `JSON.parse(JSON.stringify(paths))` deep copy — for a 30-path design with 12 nodes each, that's 360 nodes × ~200 bytes = 72KB per snapshot × 10,000 = 720MB of heap. `historyIndex` is a single number, so navigation is fine. But memory growth is real. ## Where - Workspace cap: `src/components/workspace/useWorkspace.ts:198` (the reference implementation) - No editor cap: `src/pages/Editor.tsx:670` (`pushHistory` appends without limit) - Test that would catch it: there isn't one; the editor has no test for unbounded history ## Suggested fix shape Apply the same pattern the workspace uses: ```ts const MAX_HISTORY = 50; const pushHistory = useCallback((snapshot: StitchPath[]) => { const deepSnapshot = JSON.parse(JSON.stringify(snapshot)); setHistory((prev) => { const next = [...prev, deepSnapshot]; if (next.length > MAX_HISTORY) next.shift(); return next; }); setHistoryIndex((prev) => prev + 1); // stays at "end" of trimmed stack }, []); ``` Note: the workspace increments `historyIndex` only on the user explicitly pushing (not on undo/redo). The editor's `pushHistory` is called on every state-changing dispatch, so the cap should preserve the same "you can always redo" behavior — `historyIndex` stays at the end of the trimmed stack. ## Severity Medium. Memory growth is real; the user feels it after a long session. The workspace side already has this fix; the editor side is the gap. ## Discovered by Gap analysis (2026-10-02) of editor memory safety. ## Related - #172 (open) — Workspace's history stack has no MAX_HISTORY. The reporter was right about the workspace gap; the editor has the same gap.
Author
Owner

Stale. Bug does not reproduce on main — the editor's pushHistory already caps both history and historyIndex at 50. The constant is named HISTORY_LIMIT (not MAX_HISTORY), which is why the gap-analysis grep missed it.

Evidence:

  • src/pages/Editor.tsx:265 — const HISTORY_LIMIT = 50;
  • src/pages/Editor.tsx:472-482 — pushHistory trims the array to HISTORY_LIMIT via next.slice(shiftCount) and caps historyIndex at HISTORY_LIMIT - 1 so it can never point past the trimmed end.

The cap was added in commit 674ad11 (Apply changes from kilo-gsd.py run for #67, 2026-09-13), which is older than this gap analysis (2026-10-02). The implementation is even slightly stronger than the suggested snippet: it also bounds historyIndex to HISTORY_LIMIT - 1, defending against the array-trim/index-update race.

Closing as already-resolved. Reopen if a regression is found.

Stale. Bug does not reproduce on main — the editor's `pushHistory` already caps both `history` and `historyIndex` at 50. The constant is named `HISTORY_LIMIT` (not `MAX_HISTORY`), which is why the gap-analysis grep missed it. Evidence: - `src/pages/Editor.tsx:265` — `const HISTORY_LIMIT = 50;` - `src/pages/Editor.tsx:472-482` — `pushHistory` trims the array to `HISTORY_LIMIT` via `next.slice(shiftCount)` and caps `historyIndex` at `HISTORY_LIMIT - 1` so it can never point past the trimmed end. The cap was added in commit 674ad11 (Apply changes from kilo-gsd.py run for #67, 2026-09-13), which is older than this gap analysis (2026-10-02). The implementation is even slightly stronger than the suggested snippet: it also bounds `historyIndex` to `HISTORY_LIMIT - 1`, defending against the array-trim/index-update race. Closing as already-resolved. Reopen if a regression is found.
Mark closed this issue 2026-10-04 01:05:48 +00:00
Sign in to join this conversation.
No description provided.