history_timeline_and_temporal_navigation_spec - mark-ik/graphshell GitHub Wiki
Date: 2026-03-18 (revised; originally 2026-02-28) Status: Canonical surface contract โ Stage F Priority: Implementation guidance for History Manager timeline UX
Related:
SUBSYSTEM_HISTORY.md2026-03-08_unified_history_architecture_plan.mdedge_traversal_spec.md../subsystem_storage/storage_and_persistence_integrity_spec.md
This spec defines the canonical Stage F surface contract for temporal navigation in the History subsystem. It governs:
- how preview mode is entered and exited
- which commands are blocked while preview is active
- what visual affordances mark preview as active
- scrubber/replay cursor semantics
- what "Return to present" restores
- selection and focus behavior across the preview boundary
- isolation invariants the runtime must enforce
This spec is authoritative for the History Manager timeline UX and for the
apply_history_preview_cursor / EnterHistoryTimelinePreview /
ExitHistoryTimelinePreview intent paths in history_runtime.rs.
It does not cover traversal capture correctness (see edge_traversal_spec.md)
or archive integrity (see SUBSYSTEM_HISTORY.md ยง3.2).
The app is always in one of two modes:
| Mode | State | Graph truth source |
|---|---|---|
| Live | history_preview_mode_active = false |
workspace.domain.graph (authoritative) |
| Preview | history_preview_mode_active = true |
history_preview_graph (detached replica) |
Preview is a read-only lens over a historical graph snapshot. It does not represent a branched edit; it is the past made visible without being writable.
history_preview_mode_active: bool
history_preview_live_graph_snapshot: Option<Graph> // snapshot taken at EnterPreview
history_preview_graph: Option<Graph> // current-step replica
history_replay_cursor: Option<usize> // 0 = present baseline, N = step N
history_replay_total_steps: Option<usize> // cardinality of timeline index entries
history_replay_in_progress: bool
history_last_preview_isolation_violation: bool
history_last_return_to_present_result: Option<String>
Preview is entered via GraphIntent::EnterHistoryTimelinePreview. There is one
authorised entry path:
- User selects a timeline row in the History Manager or activates the timeline scrubber.
- The UI emits
EnterHistoryTimelinePreview. - The runtime snapshots
workspace.domain.graphintohistory_preview_live_graph_snapshotand setshistory_preview_graph = snapshot.clone()(cursor = 0, the present).
After EnterHistoryTimelinePreview:
history_preview_mode_active = truehistory_preview_live_graph_snapshot = Some(live_graph_at_entry)-
history_preview_graph = Some(live_graph_at_entry)(cursor 0 = present) -
history_replay_cursor = None(scrubber not yet positioned) history_last_preview_isolation_violation = false-
CHANNEL_HISTORY_TIMELINE_PREVIEW_ENTEREDdiagnostic emitted
Both entry paths emit EnterHistoryTimelinePreview. The distinction is that
a row-click then immediately emits HistoryTimelineReplaySetTotal +
HistoryTimelineReplayAdvance to position the cursor at the selected entry.
The scrubber maintains its own position and emits advance/reset as the user
drags. Both result in the same runtime state shape.
The replay cursor is a step index over the timeline_index_entries vector,
sorted oldest-first (ascending timestamp / log_position):
- cursor = 0 โ present (baseline snapshot, before any historical step)
- cursor = 1 โ first historical event
- cursor = N โ Nth event
- cursor = total_steps โ last event (oldest visible)
The graph displayed at cursor N is the result of replay_to_timestamp over the
N-th entry in the chronological timeline index.
The timeline index now includes:
-
AppendTraversalentries (navigation events) -
AddNodestructural entries -
RemoveNodestructural entries
This means scrubbing shows structural graph history (node creation/removal) as well as traversal events. The scrubber position reflects the full WAL event history, not just navigation.
EnterHistoryTimelinePreview
โ HistoryTimelineReplaySetTotal { total_steps: N }
โ HistoryTimelineReplayAdvance { steps: K } // position at step K
โ HistoryTimelineReplayAdvance { steps: 1 } // drag one step forward
โ HistoryTimelineReplayReset // return scrubber to present
โ ExitHistoryTimelinePreview // leave preview
Advance is clamped at total_steps; advance beyond total_steps is silently
bounded (not an error).
HistoryTimelineReplayReset:
- sets
history_replay_in_progress = false - resets cursor to 0 (or
Noneiftotal_stepsis unset) - restores
history_preview_graph = history_preview_live_graph_snapshot.clone()
Reset is not exit. The app remains in preview mode at the present-baseline position. This allows the scrubber to return to "now" without exiting preview.
While history_preview_mode_active = true, any intent that falls into the
following categories is blocked and records an isolation violation instead:
-
Graph mutations โ any intent classified by
intent.as_graph_mutation(), includingAddNode,RemoveNode,AddEdge,RemoveEdge, URL updates, etc. -
Runtime events โ any intent classified by
intent.as_runtime_event(), including webview lifecycle events (navigate, load, close). -
View write actions โ the following
ViewActionvariants:SetNodePositionSetNodeFormDraftSetNodeThumbnailSetNodeFavicon
Everything not in ยง5.1 is allowed, including:
- read-only navigation (camera pan/zoom, selection changes)
- UI overlay toggles (settings, command palette, context menu)
- history manager UI operations (scrubber advance, row selection)
- diagnostic queries
- workbench layout changes that do not write to graph truth
When a blocked intent is received:
history_last_preview_isolation_violation = true-
CHANNEL_HISTORY_TIMELINE_PREVIEW_ISOLATION_VIOLATIONemitted - The intent is not applied โ no graph state change occurs
The violation is not a fatal error; preview remains active. The UI should surface the violation state (e.g., a notice in the preview overlay).
The following visual affordances mark the app as being in preview:
- Preview banner โ a persistent preview-status banner labeling the current graph state as "Viewing history" and showing the timestamp/step of the preview position. It may live in the History Manager pane or a detached overlay, but it must remain visible while preview is active without requiring row-by-row inspection of the timeline list.
- Scrubber timeline โ a horizontal scrubber bar in the History Manager pane (or detached overlay) showing position within the timeline index.
-
Return to present button โ a labelled action target that emits
ExitHistoryTimelinePreview. Must be reachable from both the banner and the scrubber area.
- Dimmed live-graph affordance โ non-preview UI chrome dims or is marked as locked during preview.
- Timestamp annotation on hovered node โ shows the WAL timestamp of the most recent event for the hovered node at the current cursor position.
- Isolation-violation toast โ transient notification when a blocked intent is attempted during preview.
Preview is exited via GraphIntent::ExitHistoryTimelinePreview.
Runtime behavior:
-
history_preview_live_graph_snapshotis taken and restored toworkspace.domain.graph. history_preview_mode_active = falsehistory_replay_in_progress = falsehistory_preview_graph = Nonehistory_last_return_to_present_result = Some("restored")-
CHANNEL_HISTORY_TIMELINE_PREVIEW_EXITEDdiagnostic emitted
If history_preview_live_graph_snapshot is None at exit (unexpected), exit
still clears preview mode โ but last_return_to_present_result is NOT set to
"restored", and CHANNEL_HISTORY_TIMELINE_RETURN_TO_PRESENT_FAILED should be
emitted.
| State | Restored? |
|---|---|
workspace.domain.graph |
Yes โ from snapshot |
| Renderer/webview live instances | Not affected (preview never touched them) |
| Camera position | Not restored (camera changes during preview are kept) |
| Node selection | Not restored (selection changes during preview are kept) |
| Workbench layout | Not affected |
| WAL | Not affected (no WAL writes occurred during preview) |
Camera and selection are intentionally not restored: if a user navigated to a different graph location while in preview, returning to present should leave them at that location, not snap them back to where they were before preview.
GraphIntent::HistoryTimelineReturnToPresentFailed { detail: String } records
a failure path. This intent exists for caller-side orchestration failures
(e.g., an external process attempted to exit preview and failed). It does not
itself clear preview mode โ the caller should emit ExitHistoryTimelinePreview
for the normal exit path and only emit ReturnToPresentFailed if the restore
itself could not be completed.
These invariants must hold at all times. Tests in ยง9 cover them.
-
No WAL writes in preview โ
log_mutationmust not be called whilehistory_preview_mode_active = true. The block-intent gate inapply_reducer_intent_internalprevents graph-mutation intents from reaching the WAL path. -
No webview lifecycle mutations in preview โ
as_runtime_event()intents are blocked (ยง5.1). The reconciler inreconcile_webview_lifecycleshould additionally checkhistory_preview_mode_activebefore acting on lifecycle deltas. -
No live graph mutations in preview โ
workspace.domain.graphmust equalhistory_preview_live_graph_snapshotat all times during preview. Any mutation path that bypasses the intent gate (direct field write, etc.) is a violation. -
Snapshot validity โ
history_preview_live_graph_snapshotmust be populated at all times whenhistory_preview_mode_active = true. If it isNoneduring preview, that is an invariant violation. -
Clean return โ After
ExitHistoryTimelinePreview, all preview fields must be cleared. Nohistory_preview_graphor snapshot leak into live state.
-
EnterHistoryTimelinePreviewsnapshots live graph, sets preview active -
ExitHistoryTimelinePreviewrestores from snapshot, clears all preview fields - Blocked intents (graph mutations, runtime events, view writes) do not mutate graph state during preview and record an isolation violation
-
HistoryTimelineReplayAdvancemoves cursor forward; graph reflects step -
HistoryTimelineReplayResetreturns cursor to 0 without exiting preview - Preview mode does not write WAL entries
- All required diagnostic channels emit on their respective events
- Isolation violation does not crash or corrupt state
Covered by existing tests in graph_app.rs:
history_health_summary_tracks_preview_and_return_to_present_failurehistory_preview_blocks_graph_mutations_and_records_isolation_violation- replay advance / set-total / reset sequence tests
history_health_summary() must expose:
preview_mode_active: boollast_preview_isolation_violation: boolreplay_cursor: Option<usize>replay_total_steps: Option<usize>replay_in_progress: boollast_return_to_present_result: Option<String>
These drive the subsystem diagnostics pane (ยง5.2 of SUBSYSTEM_HISTORY.md).
The following are explicitly deferred:
- Mixed-history timeline showing traversal + node-navigation + audit events
on the same scrubber (see
2026-03-08_unified_history_architecture_plan.md ยง6) - Node audit history surfaces
- NodeNavigationHistory timeline
- Branched or writable historical graph states ("edit from history")
- Replay of webview/renderer state alongside graph state
- Node-level timestamp annotations in the canvas during preview