feat(directives): harvest 2 directives from docs/guide_state_lifecycle.md (undo/redo 100-snapshot, reset preserves project path)

This commit is contained in:
ed
2026-07-02 23:56:33 -04:00
parent a758f0a4c9
commit 454fac1bff
4 changed files with 77 additions and 0 deletions
@@ -0,0 +1,7 @@
# undo_redo_100_snapshot_capacity
## v1
**Why this iteration:** Lifted verbatim from `docs/guide_state_lifecycle.md:56-117 (§1 Undo/Redo: HistoryManager + UISnapshot)`.
**Source:** `docs/guide_state_lifecycle.md:56-117 (§1 Undo/Redo: HistoryManager + UISnapshot)`
**Lifted:** 2026-07-02 (Phase A expansion harvest; user directive 2026-07-02)
@@ -0,0 +1,36 @@
## HistoryManager — the 100-snapshot undo/redo stack
`src/history.py:71 HistoryManager` is a 100-snapshot capacity stack with the following API:
- `push(state, description)` — appends; clears the redo stack; pops the oldest if capacity exceeded.
- `undo(current_state, current_description)` — moves current state to redo stack; returns the top of the undo stack.
- `redo(...)` — inverse of undo.
- `jump_to_undo(index, current_state, current_description)` — time-travels to any past snapshot, moving subsequent states to the redo stack.
- `can_undo`, `can_redo` properties
- `get_history()` — returns `[{description, timestamp}, ...]` for the History List view
The `max_capacity=100` is the default and is sufficient for a 5-second window of rapid typing or a longer session of infrequent edits.
## The Push Trigger — debounced change detection at render frame
The undo stack is **not** pushed on every keystroke. It's pushed via debounced change-detection at the start of every render frame:
```python
current = self._take_snapshot()
if self._last_ui_snapshot is None:
self._last_ui_snapshot = current
return
changed = (
current.ai_input != self._last_ui_snapshot.ai_input or
...
len(current.disc_entries) != len(self._last_ui_snapshot.disc_entries) or
...
)
if changed:
self.history.push(current, description="<auto>")
self._last_ui_snapshot = current
```
The check is at the start of every render frame. `copy.deepcopy(self.disc_entries)` is the most expensive part — O(N) where N is the entry count. The full snapshot push only happens when a change is detected.