# How to Implement Undo/Redo with Clypra's 100-Level History Stack Command Pattern

> Learn to implement undo redo using Clypra's 100-level history stack command pattern. Explore encapsulation, apply and invert operations for robust command management.

- Repository: [Abdulkabir Musa/Clypra](https://github.com/AIEraDev/Clypra)
- Tags: how-to-guide
- Published: 2026-07-16

---

**Clypra implements undo/redo through a command-journal architecture where every mutation is encapsulated as a Command object supporting `apply()`, `invert()`, and optional `merge()` operations, managed by the `CommandJournal` class in [`src/core/history/CommandJournal.ts`](https://github.com/AIEraDev/Clypra/blob/main/src/core/history/CommandJournal.ts) to maintain a linear history stack with a 100-level limit.**

Clypra's video editor provides robust undo/redo functionality by treating every timeline modification as a reversible command. Built on an IO-level command pattern, the system stores executed commands in the `CommandJournal`, which manages up to 100 levels of history through a centralized Zustand store at [`src/store/historyStore.ts`](https://github.com/AIEraDev/Clypra/blob/main/src/store/historyStore.ts).

## Core Architecture Components

### The Command Interface

All mutating operations in Clypra implement the Command interface defined within the history system. Each command must provide an `apply(state)` method to execute the mutation, an `invert()` method that returns a new Command representing the reverse operation, and optionally a `merge(other)` method for coalescing rapid successive actions.

### CommandJournal Implementation

The `CommandJournal` class in [`src/core/history/CommandJournal.ts`](https://github.com/AIEraDev/Clypra/blob/main/src/core/history/CommandJournal.ts) serves as the central undo/redo engine. It maintains an internal array of executed commands and a `_position` cursor tracking the current point in the history stack. When the stack exceeds 100 entries, the journal automatically truncates older commands to enforce the limit. The journal handles command execution, inverse application during undo, and reapplication during redo, while supporting transaction grouping and time-based coalescing.

## Creating Reversible Commands

Define custom operations by implementing the Command interface in the `src/core/history/commands/` directory. Here is an example of adding a clip to the timeline:

```typescript
// src/core/history/commands/AddClipCommand.ts
import type { Command } from '@/core/history/Command';
import type { Clip, TimelineState } from '@/lib/timeline/types';

export class AddClipCommand implements Command {
  public readonly label = 'Add Clip';
  public readonly undoable = true;
  public readonly timestamp = Date.now();

  constructor(private readonly clip: Clip) {}

  apply(state: TimelineState): TimelineState {
    return {
      ...state,
      clips: [...state.clips, this.clip],
    };
  }

  invert(): Command {
    return new DeleteClipCommand(this.clip.id);
  }
}

```

## Executing and Managing History

### Basic Execution Flow

UI handlers execute commands through the `historyStore` bridge. Call `historyStore.execute(command, currentState)` to apply the mutation and push it onto the history stack:

```typescript
import { useHistoryStore } from '@/store/historyStore';
import { AddClipCommand } from '@/core/history/commands/AddClipCommand';
import { getCurrentTimelineState } from '@/lib/timeline/utils';

function onAddClip(newClip: Clip) {
  const history = useHistoryStore.getState();
  const state = getCurrentTimelineState();
  
  history.execute(new AddClipCommand(newClip), state);
}

```

### Transaction Grouping

For complex operations that require multiple commands to function as a single undo step, use the transaction API. The `beginTransaction(label)` and `commitTransaction(state)` methods in [`src/store/historyStore.ts`](https://github.com/AIEraDev/Clypra/blob/main/src/store/historyStore.ts) create a composite command entry:

```typescript
import { useHistoryStore } from '@/store/historyStore';
import { DeleteClipCommand } from '@/core/history/commands/DeleteClipCommand';
import { AddClipCommand } from '@/core/history/commands/AddClipCommand';

function splitClip(originalClip: Clip, leftPart: Clip, rightPart: Clip) {
  const history = useHistoryStore.getState();
  
  history.beginTransaction('Split Clip');
  
  history.execute(new DeleteClipCommand(originalClip.id), getState());
  history.execute(new AddClipCommand(leftPart), getState());
  history.execute(new AddClipCommand(rightPart), getState());
  
  history.commitTransaction(getState());
}

```

## UI Integration and State Management

### React Component Integration

The `historyStore` exposes selectors for `canUndo`, `canRedo`, `undoLabel`, and `redoLabel` derived from the `CommandJournal` state. Use these to build responsive toolbar buttons:

```tsx
import { useHistoryStore } from '@/store/historyStore';

export function UndoRedoToolbar() {
  const { undo, redo, canUndo, canRedo, undoLabel, redoLabel } =
    useHistoryStore((state) => ({
      undo: state.undo,
      redo: state.redo,
      canUndo: state.canUndo,
      canRedo: state.canRedo,
      undoLabel: state.undoLabel,
      redoLabel: state.redoLabel,
    }));

  return (
    <div className="toolbar">
      <button disabled={!canUndo} onClick={() => undo()}>
        ↶ {undoLabel ?? 'Undo'}
      </button>
      <button disabled={!canRedo} onClick={() => redo()}>
        ↷ {redoLabel ?? 'Redo'}
      </button>
    </div>
  );
}

```

### Keyboard Shortcuts

Bind standard undo/redo shortcuts through the [`useKeyboardShortcuts.ts`](https://github.com/AIEraDev/Clypra/blob/main/useKeyboardShortcuts.ts) hook, which connects Ctrl+Z to `historyStore.undo()` and Ctrl+Y to `historyStore.redo()`:

```typescript
// src/hooks/useKeyboardShortcuts.ts
// Implements keyboard listeners that call historyStore.undo() and historyStore.redo()

```

## Summary

- **Command Pattern**: Every mutation implements the Command interface with `apply()`, `invert()`, and optional `merge()` methods located in `src/core/history/commands/`.
- **History Management**: The `CommandJournal` in [`src/core/history/CommandJournal.ts`](https://github.com/AIEraDev/Clypra/blob/main/src/core/history/CommandJournal.ts) maintains a 100-level linear stack with a `_position` cursor and automatic truncation.
- **Store Bridge**: [`historyStore.ts`](https://github.com/AIEraDev/Clypra/blob/main/historyStore.ts) wraps the journal in a Zustand interface, exposing `execute()`, `undo()`, `redo()`, and transaction helpers.
- **UI Integration**: Components consume state selectors for button states and labels, while [`useKeyboardShortcuts.ts`](https://github.com/AIEraDev/Clypra/blob/main/useKeyboardShortcuts.ts) handles keyboard bindings.
- **Transaction Support**: Group related commands using `beginTransaction()` and `commitTransaction()` to create single undoable entries.

## Frequently Asked Questions

### What is the maximum history depth in Clypra?

The `CommandJournal` enforces a 100-level limit on the history stack. When this limit is exceeded, the oldest commands are automatically removed from the bottom of the stack to maintain memory constraints while preserving recent user actions.

### How does command coalescing work in the history stack?

Commands that implement the `merge(other: Command): boolean` method can be coalesced by the `CommandJournal` when executed within a configurable time window. This collapses rapid successive operations—such as continuous drag adjustments—into a single history entry, keeping the stack clean and undo behavior intuitive.

### Can I implement custom undoable actions for plugins?

Yes. Create a new class in `src/core/history/commands/` that implements the Command interface with `apply()` and `invert()` methods, then dispatch it through `historyStore.execute()`. The system automatically integrates custom commands into the 100-level history stack with full undo/redo support.

### What happens if an error occurs during a transaction?

Transactions group multiple commands into a single composite entry. If an individual command fails during execution, the specific error handling depends on the command's implementation. However, the transaction boundary ensures that the history cursor remains consistent, preventing partial states from appearing in the undo stack.