# Building Undo/Redo for Complex Timeline Mutations in Palmier Pro

> Learn how Palmier Pro builds undo redo for complex timeline mutations. Discover EditorUndo and its unified transaction model for UI actions and agent tools.

- Repository: [Palmier/palmier-pro](https://github.com/palmier-io/palmier-pro)
- Tags: internals
- Published: 2026-07-27

---

**Palmier Pro implements a robust undo/redo system using `EditorUndo`, a thin wrapper around Foundation's `UndoManager` that unifies UI actions and Agent-driven tools under a single coherent transaction model.**

Palmier Pro's timeline editing capabilities rely on a sophisticated mutation system that must remain reversible across both manual user interactions and automated AI-driven tools. The source code in `palmier-io/palmier-pro` demonstrates how to build an undo architecture that handles complex, multi-step timeline operations while maintaining a clean, actionable history stack.

## Core Architecture: The EditorUndo Wrapper

The foundation of Palmier Pro's undo system is `EditorUndo`, located in [`Sources/PalmierPro/Editor/EditorUndo.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Editor/EditorUndo.swift). This class provides a transactional layer over `Foundation.UndoManager`, adding editor-specific semantics for grouping related mutations into single user-visible actions. Each editor instance maintains its own `EditorUndo` object, typically held in [`EditorViewModel.swift`](https://github.com/palmier-io/palmier-pro/blob/main/EditorViewModel.swift) as an `@ObservationIgnored` property to prevent SwiftUI observation cycles while keeping the manager accessible throughout the view hierarchy.

### Attachment and Lifecycle Management

Before processing any mutations, the wrapper must attach to an existing `UndoManager` instance supplied by the UI or test harness. The `attach(_:)` method stores a weak reference to the manager and prepares the wrapper for transaction handling.

```swift
// Sources/PalmierPro/Editor/EditorUndo.swift lines 9-12
func attach(_ manager: UndoManager?) {
    self.manager = manager
}

```

This design allows tests to inject fresh `UndoManager` instances while the production app uses the system-provided manager from the responder chain.

### Transactional Execution with perform()

The `perform(_:_:)` method (lines 13-36) serves as the primary entry point for timeline mutations requiring undo support. This method ensures **atomicity** by managing undo groupings automatically.

When invoked, `perform` first guards against re-entrancy by checking if an undo or redo operation is already in progress. It then temporarily disables `groupsByEvent` to prevent Foundation from automatically creating separate undo groups for each event. The method tracks `transactionActive` and `transactionGroupOpened` states to ensure the grouping level is restored even if the mutation throws an error.

```swift
// Conceptual usage from the Palmier Pro source
editor.undo.perform("Ripple Trim") {
    // Complex timeline mutation logic here
    for clip in affectedClips {
        clip.trim(start: newStart)
        editor.undo.register("Ripple Trim", withTarget: clip) { $0.trim(start: oldStart) }
    }
}

```

## Registering Inverse Operations for Timeline Mutations

During an active transaction, individual model objects register their specific inverse operations using `register(_:withTarget:handler:)` (lines 39-54). This method checks whether a transaction is already active; if so, it opens a grouping once and registers the block. If no transaction is active, it recursively invokes `perform` to ensure the registration occurs under the correct action name.

This mechanism is crucial for complex timeline mutations that touch multiple clips, markers, and effects. Rather than creating dozens of separate undo steps, the wrapper aggregates all registrations under the single action name provided to `perform`.

### Selective Registration and Stack Management

Not all state changes should be reversible. Preview generation, live waveform updates, and transient UI states must mutate the model without polluting the undo stack. The `withoutRegistration(_:)` method (lines 56-63) temporarily disables undo registration by calling `manager.disableUndoRegistration()` before executing the provided closure, then restoring the previous state afterward.

```swift
// Sources/PalmierPro/Editor/EditorUndo.swift lines 56-63
func withoutRegistration(_ operation: () throws -> Void) rethrows {
    manager?.disableUndoRegistration()
    defer { manager?.enableUndoRegistration() }
    try operation()
}

```

## Unified Undo History Across UI and Agent Tools

Palmier Pro distinguishes itself by allowing both human editors and AI Agents to manipulate the timeline through the same code paths. The undo system treats both sources identically, ensuring that an "Undo" command correctly reverses the last mutation regardless of whether it originated from a slider in the inspector or an autonomous tool execution.

### UI-Driven Mutations

Inspector controls in [`Sources/PalmierPro/Inspector/InspectorView.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Inspector/InspectorView.swift) (lines 701-720) wrap value changes in `perform` blocks. For example, when adjusting a clip's opacity, the view calls:

```swift
editor.undo.perform("Change Opacity") {
    clip.opacity = newValue
    editor.undo.register("Change Opacity", withTarget: clip) { $0.opacity = oldValue }
}

```

### Agent-Driven Tools

AI tools in `Sources/PalmierPro/Agent/Tools/ToolExecutor+Words.swift` (line 105) utilize the same API. When the "Remove Silence" agent processes audio, it creates a transaction that encompasses all individual clip trims:

```swift
try editor.undo.perform("Remove Silence (Agent)") {
    for clip in clipsToProcess {
        let originalRange = clip.audioRange
        clip.removeSilence()
        editor.undo.register("Remove Silence (Agent)", withTarget: clip) {
            $0.audioRange = originalRange
        }
    }
}

```

This ensures that undoing an Agent operation restores the timeline to its exact previous state, with all per-clip changes rolling back atomically.

## Safety Mechanisms for Complex Edits

### Re-entrancy Safety

The wrapper guards against recursive registration during undo/redo operations. If `perform` is called while an undo or redo is in progress (detected via `manager.isUndoing` or `isRedoing`), it early-exits to prevent corrupting the stack. This protection is essential when timeline mutations trigger cascading updates that might otherwise attempt to register new undo actions during the restoration process.

### Event-Group Isolation

Foundation's `UndoManager` automatically groups actions by event run-loop cycles when `groupsByEvent` is true. For complex timeline mutations involving multiple async boundaries or UI updates, this would incorrectly split a logical operation into multiple undo steps. `EditorUndo` explicitly disables this behavior during transactions, ensuring that the entire sequence of registrations collapses into a single named entry.

### Retrieval and Execution

To provide contextual UI feedback (such as menu items displaying "Undo Ripple Trim"), the `undoLatest()` method (lines 67-72) returns the name of the action about to be undone before invoking `manager.undo()`. This allows the UI layer to display meaningful action names without parsing private undo stack contents.

```swift
if let actionName = editor.undo.undoLatest() {
    // Returns "Ripple Trim" and performs the undo
    statusBar.showMessage("Undid: \(actionName)")
}

```

## Testing the Undo System

The test suite in [`Tests/PalmierProTests/Editor/EditorUndoTests.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Tests/PalmierProTests/Editor/EditorUndoTests.swift) (lines 24-30) validates transactional semantics through a dedicated harness that returns a tuple of `(EditorUndo, UndoManager, UndoCounter)`. This setup allows precise verification that:

- Grouping levels are correctly maintained across nested transactions
- `withoutRegistration` prevents stack pollution
- `undoLatest` returns the expected action names
- Agent and UI mutations produce identical undo behavior

[`Sources/PalmierPro/Timeline/TimelineInputController.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Timeline/TimelineInputController.swift) demonstrates production usage of these patterns, handling drag gestures and playhead movements through the unified `perform` API.

## Summary

- **`EditorUndo`** wraps `Foundation.UndoManager` to provide transactional semantics for timeline mutations in [`Sources/PalmierPro/Editor/EditorUndo.swift`](https://github.com/palmier-io/palmier-pro/blob/main/Sources/PalmierPro/Editor/EditorUndo.swift).
- **Atomic grouping** via `perform(_:_:)` ensures complex multi-clip operations appear as single undo steps with meaningful names.
- **Re-entrancy protection** prevents recursive registration during undo/redo execution, maintaining stack integrity.
- **Selective registration** using `withoutRegistration(_:)` allows preview updates and transient state changes without polluting the history.
- **Unified API** enables both UI components ([`InspectorView.swift`](https://github.com/palmier-io/palmier-pro/blob/main/InspectorView.swift)) and Agent tools (`ToolExecutor+Words.swift`) to share a coherent undo history.
- **Action retrieval** through `undoLatest()` supports contextual UI feedback by exposing the name of the operation being reversed.

## Frequently Asked Questions

### How does Palmier Pro handle undo/redo for AI-driven timeline edits?

Palmier Pro treats Agent-driven mutations identically to user-driven edits by routing both through the `EditorUndo.perform` method. When an AI tool like "Remove Silence" executes in `ToolExecutor+Words.swift`, it opens a transaction with a descriptive name (e.g., "Remove Silence (Agent)") and registers inverse operations for each affected clip. This ensures that undoing an Agent edit reverts all timeline changes atomically, just like undoing a manual trim operation.

### What prevents recursive undo registration during complex timeline mutations?

The `perform` method in [`EditorUndo.swift`](https://github.com/palmier-io/palmier-pro/blob/main/EditorUndo.swift) checks `manager.isUndoing` and `isRedoing` before processing new registrations. If an undo or redo is already in progress, the method returns early without executing the closure. This re-entrancy guard is critical for preventing the corruption that would occur if restoration code inadvertently added new entries to the stack while traversing existing ones.

### How can I exclude preview updates from the undo stack in Palmier Pro?

Use the `withoutRegistration(_:)` method to wrap any code that should not generate undo history. This method calls `disableUndoRegistration()` on the underlying `UndoManager`, executes the provided closure, and then restores the registration state. It is commonly used for generating waveform previews or updating transient UI state that reflects the current timeline without changing its logical history.

### Where is the undo manager initialized in the Palmier Pro editor lifecycle?

The `EditorUndo` instance is created as a property of `EditorViewModel` (specifically marked with `@ObservationIgnored` to prevent SwiftUI re-renders) and attached to the responder chain's `UndoManager` via `attach(_:)` when the editor activates. This lifecycle ensures that each editor window maintains isolated undo history, and tests can inject mock managers by calling `attach` with a fresh `UndoManager` instance before executing mutations.