Debug Sessions & Replay
The tui-dispatch-debug crate provides tooling for headless debugging, action recording/replay, and JSON schema generation. These features are particularly useful for automated testing and LLM-assisted development.
Overview
The debug session system enables:
- Recording: Capture all dispatched actions to a JSON file
- Replay: Load and replay actions, including async coordination
- Snapshots: Save/load state as JSON
- Schema Generation: Produce JSON schemas for state and actions
- Headless Mode: Render once and exit for CI/testing
CLI Arguments
DebugCliArgs integrates with clap for standard debug flags:
use clap::Parser;use tui_dispatch::debug::DebugCliArgs;
#[derive(Parser)]struct Args { #[clap(flatten)] debug: DebugCliArgs,
// Your other args...}Available flags:
| Flag | Description |
|---|---|
--debug | Enable debug mode (F12 overlay) |
--debug-render-once | Render one frame and exit |
--debug-state-in <PATH> | Load initial state from JSON |
--debug-actions-in <PATH> | Load and replay actions from JSON |
--debug-actions-out <PATH> | Save dispatched actions to JSON |
--debug-actions-include <PATTERNS> | Include patterns (comma-separated globs) |
--debug-actions-exclude <PATTERNS> | Exclude patterns (comma-separated globs) |
--debug-state-schema-out <PATH> | Save state JSON schema |
--debug-actions-schema-out <PATH> | Save actions JSON schema |
--debug-replay-timeout <SECS> | Timeout for async awaits (default: 30) |
DebugSession
DebugSession orchestrates the CLI flags into your app:
use tui_dispatch::debug::{DebugCliArgs, DebugSession};
let args = DebugCliArgs::parse();let session = DebugSession::new(args);
// Check if debug mode is enabledif session.enabled() { // Configure debug layer}
// Check for render-once mode (CI/testing)if session.render_once() { // Will exit after first render}Loading State
Load initial state from a JSON file, with a fallback for normal startup:
let state = session.load_state_or_else(|| { // Normal initialization Ok(AppState::default())})?;
// Or with async initialization:let state = session.load_state_or_else_async(|| async { Ok(AppState::fetch_initial().await)}).await?;Recording Actions
Record dispatched actions for later replay:
// Create middleware that records actionslet (middleware, recorder) = session.middleware_with_recorder::<Action>();
// Use with your storelet mut store = StoreWithMiddleware::new(state, reducer, middleware);
// ... run your app ...
// Save recorded actions when donesession.save_actions(&recorder)?;The recorder respects --debug-actions-include and --debug-actions-exclude patterns.
Replay Items
Actions can be replayed from a JSON file. The format supports both simple action arrays and replay items with async coordination:
Simple Format
[ "Increment", "Increment", {"SetValue": 42}, "Submit"]With Await Markers
For async operations, use _await to pause replay until an action is dispatched:
[ {"UserFetch": "octocat"}, {"_await": "UserDidLoad"}, "ViewProfile"]The replay pauses after UserFetch and waits until UserDidLoad (or UserDidError) is dispatched by your effect handler.
Await Any
Wait for any of several possible actions:
[ {"WeatherFetch": {"lat": 51.5, "lon": -0.1}}, {"_await_any": ["WeatherDidLoad", "WeatherDidError"]}, "ShowDetails"]Loading Replay Items
let replay_items = session.load_replay_items::<Action>()?;
// replay_items: Vec<ReplayItem<Action>>// - ReplayItem::Action(a) - dispatch this action// - ReplayItem::AwaitOne { _await } - wait for matching action// - ReplayItem::AwaitAny { _await_any } - wait for any matchJSON Schema Generation
Generate JSON schemas for your state and action types. Requires the json-schema feature:
[dependencies]tui-dispatch = { version = "0.5", features = ["json-schema"] }#[cfg(feature = "json-schema")]{ // Save state schema session.save_state_schema::<AppState>()?;
// Save actions schema (includes awaitable_actions list) session.save_actions_schema::<Action>()?;}Your types need to derive JsonSchema:
use schemars::JsonSchema;use serde::{Deserialize, Serialize};
#[derive(Default, Serialize, Deserialize, JsonSchema)]struct AppState { count: i32, user: Option<User>,}
#[derive(Action, Clone, Debug, Serialize, Deserialize, JsonSchema)]enum Action { Increment, UserFetch(String), UserDidLoad(User), UserDidError(String),}The actions schema includes an awaitable_actions extension listing actions containing “Did” (async completion markers).
Complete Integration Example
Here’s a full example integrating debug session into an effect-based app:
use clap::Parser;use std::io;use tui_dispatch::prelude::*;use tui_dispatch::debug::{DebugCliArgs, DebugLayer, DebugSession};
#[derive(Parser)]struct Args { #[clap(flatten)] debug: DebugCliArgs,}
#[tokio::main]async fn main() -> io::Result<()> { let args = Args::parse(); let session = DebugSession::new(args.debug);
// Load state (from file or default) let state = session.load_state_or_else(|| Ok(AppState::default()))?;
// Set up middleware with optional recorder let (middleware, recorder) = session.middleware_with_recorder::<Action>();
// Load replay items (empty if no --debug-actions-in) let replay_items = session.load_replay_items::<Action>()?;
// Create store and debug layer let mut store = EffectStoreWithMiddleware::new(state, reducer, middleware); let debug_layer = if session.enabled() { DebugLayer::simple().active(true) } else { DebugLayer::simple() };
let mut bus: SimpleEventBus<AppState, Action, AppComponentId> = SimpleEventBus::new(); let keybindings: Keybindings<DefaultBindingContext> = Keybindings::new();
// Set up terminal... let mut terminal = setup_terminal()?;
// Run with replay support let output = session.run_effect_app_with_bus( &mut terminal, store, debug_layer, replay_items, session.auto_fetch().then_some(Action::AutoFetch), Some(Action::Quit), |_runtime| {}, &mut bus, &keybindings, |frame, area, state, ctx, event_ctx| render(frame, area, state, ctx, event_ctx), |action| matches!(action, Action::Quit), |effect, ctx| handle_effect(effect, ctx), ).await?;
// Cleanup terminal... restore_terminal(&mut terminal)?;
// Save recorded actions session.save_actions(&recorder)?;
// Save schemas if requested #[cfg(feature = "json-schema")] { session.save_state_schema::<AppState>()?; session.save_actions_schema::<Action>()?; }
Ok(())}LLM-Assisted Debugging Workflow
The debug session tooling enables a powerful workflow for LLM-assisted development:
1. Record a Session
Run your app and interact with it normally:
cargo run -- --debug-actions-out session.json2. Generate Schemas
cargo run --features json-schema -- \ --debug-state-schema-out state-schema.json \ --debug-actions-schema-out actions-schema.json \ --debug-render-once3. Share with LLM
Provide the LLM with:
state-schema.json- describes your state structureactions-schema.json- describes available actions and which are awaitablesession.json- example of recorded interactions
4. LLM Generates Replay
The LLM can generate a replay file:
[ {"SearchQuery": "rust tui"}, {"_await": "SearchDidComplete"}, {"SearchSelect": 0}, "Confirm"]5. Replay and Verify
cargo run -- \ --debug-actions-in llm-generated.json \ --debug-render-once \ --debug-replay-timeout 10The app will:
- Dispatch
SearchQuery("rust tui") - Wait up to 10 seconds for
SearchDidComplete - Dispatch
SearchSelect(0) - Dispatch
Confirm - Render final state and exit
Error Handling
Replay errors are reported clearly:
pub enum ReplayError { Timeout { pattern: String }, // Await timed out ChannelClosed, // Action channel closed}If an _await times out, you’ll see which pattern was being waited for.
See Also
- Debug Layer - Interactive F12 overlay
- Middleware - Action recording via middleware
- Async Patterns - Effect-based apps with Did* actions