PURPOSE

Command-pattern undo/redo stack for the Playground designer screen. Each parameter change (slider, dropdown, toggle) becomes a HistoryCommand with execute() and undo(). Provides multi-level undo/redo capped at 200 commands, a timestamped change log for designer review, and a session-start snapshot for full reset. Slider drags are debounced upstream: commands are created on pointerUp, not on every onChange tick.

OWNS

  • HistoryCommand interface — key, label, prev, next, timestamp, execute(), undo().
  • HistoryStack interface — commands[], pointer, sessionSnapshot.
  • MAX_HISTORY constant capping the stack at 200 commands.
  • Stack factory createHistoryStack() and snapshot setter saveSessionSnapshot().
  • Stack operations pushCommand(), undo(), redo(), canUndo(), canRedo(), clearHistory().
  • Log readers getLog() (commands up to pointer) and getFullLog() (all commands, each tagged with isUndone).
  • Command factory createValueCommand() that mutates a state object at a dotted key path (e.g. palette.base).
  • Display formatters formatValue() (numbers trim trailing zeros, booleans render ON/OFF, strings pass through, other types stringify as JSON) and formatTime() (HH:MM:SS).

READS FROM

  • Date.now() for command timestamps in createValueCommand().
  • structuredClone() for the session-start snapshot deep copy.
  • The caller-supplied state object inside createValueCommand — walked by dotted key path on execute/undo.

PUSHES TO

  • The caller-supplied state object — execute() writes next, undo() restores prev via mutation at the resolved key path.
  • The HistoryStack passed to pushCommand()/undo()/redo()/clearHistory() — mutates commands and pointer in place; trims the redo tail when a new command is pushed past a partial undo; shifts the oldest command and decrements the pointer when MAX_HISTORY is reached.
  • stack.sessionSnapshotsaveSessionSnapshot() deep-clones the provided state into the field.

DOES NOT

  • Does not perform the parameter mutation itself outside the supplied execute/undo closures — callers build the command and the file just runs it.
  • Does not debounce slider input — debouncing is the caller’s responsibility (commands are expected to arrive on pointerUp).
  • Does not restore from sessionSnapshot — the snapshot is stored but applying it is the caller’s job.
  • Does not bind to keyboard shortcuts, render any UI, or coalesce adjacent commands with the same key.
  • Does not validate that the dotted key path in createValueCommand exists on the state object — missing intermediate nodes will throw at mutation time.
  • Does not emit events or notify listeners — consumers re-read stack state after each operation.
  • Does not persist the stack across reloads.

Signals

  • pushCommand() truncates commands to pointer + 1 before appending, discarding any redo tail.
  • When the stack reaches MAX_HISTORY, pushCommand() drops the oldest entry via shift() and decrements pointer so the live position stays aligned.
  • undo() returns false when pointer < 0; redo() returns false when at the head of the stack — both are safe to call repeatedly.
  • getFullLog() returns the entire stack with an isUndone flag (i > pointer) for grayed-out display of redoable commands.
  • formatValue() trims trailing zeros from numbers with the regex /\.?0+$/ after toFixed(3), so 1.500 renders as 1.5 and 2.000 renders as 2.

Entry points

  • Playground screen modules that wire sliders/toggles to parameter state instantiate one HistoryStack via createHistoryStack(), call saveSessionSnapshot() on mount, and route every committed change through createValueCommand() + pushCommand().
  • Undo/redo UI buttons call undo(stack) / redo(stack) and gate enablement on canUndo(stack) / canRedo(stack).
  • Change-log panels render getFullLog(stack) using formatValue() and formatTime() for display.
  • “Reset to Session Start” actions read stack.sessionSnapshot (snapshot apply lives in caller code).

Pattern notes

  • Pure command-pattern implementation — the file holds no module-level state; each HistoryStack is an owned plain object the caller passes around explicitly.
  • All operations are mutating functions on a passed-in stack, not methods on a class — consistent with the project’s data-table-plus-functions style.
  • Commands store prev/next as unknown, so the stack is type-agnostic: numbers, strings, booleans, and nested objects all flow through the same path.
  • createValueCommand() builds the mutation closures once at construction time, capturing parts and stateexecute/undo then walk the path on each invocation rather than caching the parent node, so reassignment of intermediate objects between operations is tolerated.
  • Redo tail truncation uses commands.length = stack.pointer + 1 rather than splice, avoiding the intermediate array allocation.
  • The 200-command cap is a hard ceiling rather than a soft hint — overflow is silently dropped from the head.