Kanban CLI supports undo (u) and redo (U) operations for all state-modifying actions. The history system uses a snapshot-based approach with bounded memory to track up to 100 recent operations.
The HistoryManager maintains separate stacks for undo and redo:
pub struct HistoryManager { /// Stack of snapshots for undo (most recent = back of deque) undo_stack: VecDeque<Snapshot>, /// Stack of snapshots for redo (most recent = back of deque) redo_stack: VecDeque<Snapshot>, /// Flag to prevent undo/redo operations from being added to history suppress_capture: bool,}
pub fn undo(&mut self) -> Result<()> { if let Some(snapshot) = self.history.pop_undo() { // Save current state for redo let current = self.create_snapshot(); // Suppress capture during restore self.history.suppress(); // Restore previous state self.restore_snapshot(snapshot); // Re-enable capture self.history.unsuppress(); // Push current to redo stack self.history.push_redo(current); Ok(()) } else { Err("Nothing to undo") }}
pub fn redo(&mut self) -> Result<()> { if let Some(snapshot) = self.history.pop_redo() { // Save current state for undo let current = self.create_snapshot(); // Suppress capture during restore self.history.suppress(); // Restore forward state self.restore_snapshot(snapshot); // Re-enable capture self.history.unsuppress(); // Push current to undo stack self.history.push_undo(current); Ok(()) } else { Err("Nothing to redo") }}
The implementation follows standard undo/redo semantics:
1
New Action Clears Redo
When a new action is performed after an undo, the redo stack is cleared.
pub fn capture_before_command(&mut self, snapshot: Snapshot) { if self.suppress_capture { return; } self.undo_stack.push_back(snapshot); // Any new action clears the redo history self.redo_stack.clear();}
2
Undo Enables Redo
Each undo operation saves the current state to the redo stack.
3
Redo Enables Undo
Each redo operation saves the current state to the undo stack.
// Approximate memory per snapshot:// - ~1KB per board// - ~0.5KB per column// - ~2KB per card (with description)// - ~1KB per sprint// - ~0.5KB per dependency edge// Example: Board with 50 cards, 5 columns, 3 sprints:// ~115KB per snapshot// ~11.5MB for 100 snapshots
The bounded history prevents unbounded growth while providing sufficient depth for typical workflows.
// User creates a cardlet card = create_card("New Feature");// Snapshot captured automatically// User edits the cardcard.update_title("Improved Feature");// New snapshot captured// User presses 'u' to undoundo();// Restores title to "New Feature"// Current state saved to redo stack// User presses 'u' againundo();// Card is removed (undoes creation)// User presses 'U' to redoredo();// Card is recreated with "New Feature"// User presses 'U' againredo();// Title changes to "Improved Feature"// User creates a new cardlet card2 = create_card("Another Feature");// Redo stack is cleared (standard undo/redo behavior)