# Adding Undo/Redo Support to Pascal Editor Operations with Zundo: Complete Implementation Guide

> Implement undo redo support in your Pascal Editor operations using Zundo. Learn how to wrap your Zustand store with temporal enhancer for automatic state snapshots and actions.

- Repository: [Pascal/editor](https://github.com/pascalorg/editor)
- Tags: how-to-guide
- Published: 2026-03-25

---

**Pascal Editor implements robust undo/redo functionality by wrapping its Zustand store with the Zundo library's `temporal()` enhancer, which automatically records state snapshots and exposes `undo()` and `redo()` actions across keyboard shortcuts, command palette entries, and tool-specific workflows.**

Pascal Editor manages a complex, immutable scene graph through a centralized Zustand store, where every node operation must be reversible. By integrating Zundo's temporal state management, the editor records granular history snapshots without boilerplate, enabling developers to add undo/redo support to Pascal Editor operations with Zundo seamlessly across all editor interactions.

## Core Architecture: The Temporal Store Wrapper

The foundation of Pascal Editor's history system lies in the store configuration at [`packages/core/src/store/use-scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/store/use-scene.ts). Here, the Zustand store is enhanced with Zundo's `temporal()` wrapper to create a time-travel capable state container.

### Configuring the Temporal State Interface

The TypeScript definition extends the standard Zustand store with a `temporal` sub-store that manages history stacks for specific state slices:

```typescript
import type { TemporalState } from 'zundo'
import { temporal } from 'zundo'

type UseSceneStore = UseBoundStore<StoreApi<SceneState>> & {
  temporal: StoreApi<
    TemporalState<Pick<SceneState, 'nodes' | 'rootNodeIds' | 'collections'>>
  >
}

const useScene: UseSceneStore = create<SceneState>()(
  temporal(
    (set, get) => ({
      /* …initial state & actions… */
    })
  )
)

```

This configuration selectively tracks only **mutable scene data**—`nodes`, `rootNodeIds`, and `collections`—while ignoring transient UI state like selection highlights or viewport position.

### Automatic Snapshotting on State Mutations

Zundo automatically pushes snapshots onto the history stack whenever tracked slices change. When store actions like `createNode`, `updateNode`, or `deleteNode` execute, Zundo captures the previous state before applying mutations. Calling `undo()` restores the prior snapshot and moves the current state onto a redo stack, enabling full history traversal without manual state cloning or deep-copy logic.

## Integration Points for Undo/Redo Support

Pascal Editor exposes the temporal API through three primary integration points, ensuring consistent history navigation across interaction patterns.

### Keyboard Shortcuts in use-keyboard.ts

Global keyboard handlers in [`packages/editor/src/hooks/use-keyboard.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/hooks/use-keyboard.ts) bind standard history commands to Zundo's temporal methods. The implementation detects platform-specific modifiers and invokes the appropriate temporal action:

```typescript
// In packages/editor/src/hooks/use-keyboard.ts (lines 56-62)
useScene.temporal.getState().undo()  // ⌘/Ctrl + Z
useScene.temporal.getState().redo()  // ⇧⌘/Ctrl + Shift + Z

```

This provides instantaneous history navigation during active editing sessions.

### Command Palette Commands

The command palette in [`packages/editor/src/components/ui/command-palette/editor-commands.tsx`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/components/ui/command-palette/editor-commands.tsx) registers "Undo" and "Redo" as searchable commands. Each entry's `execute` callback accesses `useScene.temporal.getState()` directly, ensuring that palette-driven history actions remain synchronized with keyboard shortcuts. This pattern appears at lines 97-104 of the source file, where commands invoke `undo()` or `redo()` without additional abstraction layers.

### Tool-Level Pause and Resume for Draft Operations

Complex tools require **temporal isolation** during multi-step operations. When creating draft nodes or performing drag gestures, tools call `pause()` to suspend history recording, preventing intermediate states from polluting the undo stack.

In [`packages/editor/src/components/tools/item/use-draft-node.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/components/tools/item/use-draft-node.ts) (lines 43-56), the implementation demonstrates this pattern:

```typescript
useScene.temporal.getState().pause()
// …mutate draft node position…
useScene.temporal.getState().resume()

```

Only the final committed state appears in history, while drag increments remain invisible. The [`move-window-tool.tsx`](https://github.com/pascalorg/editor/blob/main/move-window-tool.tsx) file implements similar logic for window repositioning operations.

## Implementation Code Examples

### Basic Undo/Redo Buttons

Connect components directly to the temporal API for custom history controls:

```tsx
import { useScene } from '@pascal-app/core'

export const UndoRedoButtons = () => {
  const { undo, redo } = useScene.temporal.getState()

  return (
    <div className="flex gap-2">
      <button onClick={undo}>Undo ⌘Z</button>
      <button onClick={redo}>Redo ⇧⌘Z</button>
    </div>
  )
}

```

### Custom Editor Command Registration

Add history commands to the palette by implementing the `Command` interface:

```tsx
import { useScene } from '@pascal-app/core'
import { Command } from './command-types'

export const myUndoCommand: Command = {
  id: 'my.history.undo',
  label: 'Undo',
  group: 'History',
  icon: <Undo2 />,
  execute: () => useScene.temporal.getState().undo(),
}

```

### Complex Tool with Temporal Control

Isolate draft states from history during multi-step operations:

```typescript
import { useScene } from '@pascal-app/core'

export const moveNode = (id: string, newPos: [number, number, number]) => {
  const { pause, resume, getState } = useScene.temporal.getState()

  pause()                     // Stop history recording
  getState().updateNode(id, { position: newPos })
  resume()                    // Record final state as single entry
}

```

## Summary

- **Temporal wrapper**: Zundo's `temporal()` enhancer in [`use-scene.ts`](https://github.com/pascalorg/editor/blob/main/use-scene.ts) automatically tracks `nodes`, `rootNodeIds`, and `collections` without manual snapshot management.
- **Multiple access points**: History navigation is available via keyboard shortcuts ([`use-keyboard.ts`](https://github.com/pascalorg/editor/blob/main/use-keyboard.ts)), command palette entries ([`editor-commands.tsx`](https://github.com/pascalorg/editor/blob/main/editor-commands.tsx)), and direct API calls.
- **Draft operation support**: The `pause()` and `resume()` methods enable tools to suppress intermediate states during complex interactions like drag-and-drop.
- **Immutable history**: Each undo operation restores previous immutable snapshots, while redo maintains a forward stack for bidirectional navigation.

## Frequently Asked Questions

### How does Zundo track state changes in Pascal Editor?

Zundo automatically intercepts state mutations performed through the Zustand store's setter functions. When actions like `createNode` or `updateNode` modify the tracked slices (`nodes`, `rootNodeIds`, `collections`), Zundo pushes the pre-mutation state onto an internal history array before applying changes. This mechanism requires no additional instrumentation in the action definitions themselves.

### Can I exclude specific state properties from undo history?

Yes. The `temporal()` configuration in [`packages/core/src/store/use-scene.ts`](https://github.com/pascalorg/editor/blob/main/packages/core/src/store/use-scene.ts) uses TypeScript's `Pick` utility to explicitly whitelist only `nodes`, `rootNodeIds`, and `collections`. Properties omitted from this selection—such as UI preferences, viewport camera positions, or selection highlights—mutate without generating history entries, keeping the undo stack focused on scene content changes.

### How do I prevent draft operations from creating multiple history entries?

Use the `pause()` and `resume()` methods available on `useScene.temporal.getState()`. Call `pause()` before initiating draft mutations (such as dragging a window or resizing a node), perform all intermediate updates, then call `resume()` before the final commit. This pattern, demonstrated in [`use-draft-node.ts`](https://github.com/pascalorg/editor/blob/main/use-draft-node.ts) and [`move-window-tool.tsx`](https://github.com/pascalorg/editor/blob/main/move-window-tool.tsx), ensures only the final committed state appears as a single undoable entry.

### Where are the keyboard shortcuts for undo/redo implemented?

Global keyboard shortcuts reside in [`packages/editor/src/hooks/use-keyboard.ts`](https://github.com/pascalorg/editor/blob/main/packages/editor/src/hooks/use-keyboard.ts) (lines 56-62), where the handler detects `⌘/Ctrl + Z` for undo and `⇧⌘/Ctrl + Shift + Z` for redo. These shortcuts directly invoke `useScene.temporal.getState().undo()` and `redo()`, bypassing any intermediate dispatch layers for immediate response.