Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 0 additions & 8 deletions check_write.rs

This file was deleted.

103 changes: 94 additions & 9 deletions src/bin/gui.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,16 +14,22 @@
});

// Enhanced GUI app with comprehensive features
struct TimeLoopGui {
// AI communication
ai_sender: Sender<Result<String, String>>,
ai_receiver: Receiver<Result<String, String>>,
// Timeline visualization constants
const TIMELINE_HEIGHT: f32 = 60.0;
const TIMELINE_BG_COLOR: egui::Color32 = egui::Color32::from_rgb(30, 30, 30);
const COLOR_COMMAND: egui::Color32 = egui::Color32::from_rgb(100, 149, 237); // Cornflower Blue
const COLOR_FILE_CHANGE: egui::Color32 = egui::Color32::from_rgb(255, 99, 71); // Tomato Red
const COLOR_TERMINAL: egui::Color32 = egui::Color32::from_rgb(100, 100, 100);
const COLOR_KEYPRESS: egui::Color32 = egui::Color32::from_rgb(60, 60, 60);
const COLOR_METADATA: egui::Color32 = egui::Color32::WHITE;

struct TimeLoopGui {
// Session management
session_manager: Option<SessionManager>,
sessions: Vec<timeloop_terminal::session::Session>,
selected: Option<String>,
replay_summary: Option<timeloop_terminal::replay::ReplaySummary>,
replay_events: Vec<Event>,

Check failure

Code scanning / clippy

cannot find type Event in this scope Error

cannot find type Event in this scope

// Replay controls
playing: bool,
Expand Down Expand Up @@ -60,7 +66,6 @@

impl Default for TimeLoopGui {
fn default() -> Self {
let (tx, rx) = std::sync::mpsc::channel();
let mut sessions = Vec::new();
let session_manager = SessionManager::new().ok();

Expand All @@ -82,6 +87,7 @@
sessions,
selected: None,
replay_summary: None,
replay_events: Vec::new(),
playing: false,
speed: 1.0,
position_ms: 0,
Expand Down Expand Up @@ -413,9 +419,16 @@

// Load replay summary
if let Ok(engine) = ReplayEngine::new(session_id) {
if let Ok(rs) = engine.get_session_summary() {
self.replay_summary = Some(rs);
if let Ok(events) = engine.get_events() {
self.replay_summary = Some(ReplayEngine::calculate_summary(&events));
self.replay_events = events;
} else {
self.replay_events.clear();
self.replay_summary = None;
}
} else {
self.replay_events.clear();
self.replay_summary = None;
}
Comment on lines 421 to 432
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

This block for loading session data has a significant performance issue and can be made more robust.

Performance: engine.get_events() and engine.get_session_summary() both internally read and parse all events from storage. This means you are doing the same expensive work twice every time a session is selected. For large sessions, this will be slow. I recommend refactoring ReplayEngine to calculate the summary from the already-loaded events to avoid this.

Robustness: The error handling can lead to an inconsistent UI state and can be simplified. The suggested change improves robustness, but the performance issue will remain until ReplayEngine is refactored.

        if let Ok(engine) = ReplayEngine::new(session_id) {
            if let Ok(events) = engine.get_events() {
                self.replay_summary = engine.get_session_summary().ok();
                self.replay_events = events;
            } else {
                self.replay_events.clear();
                self.replay_summary = None;
            }
        } else {
            self.replay_events.clear();
            self.replay_summary = None;
        }

}

Expand Down Expand Up @@ -639,8 +652,80 @@
// Timeline visualization
ui.group(|ui| {
ui.heading("📈 Timeline");
ui.label("Event timeline visualization would go here");
// TODO: Implement actual timeline visualization
let (rect, _response) = ui.allocate_exact_size(
egui::vec2(ui.available_width(), TIMELINE_HEIGHT),
egui::Sense::hover(),
);

// Draw timeline background
ui.painter().rect_filled(rect, 4.0, TIMELINE_BG_COLOR);

let total_duration_ms = rs.duration.num_milliseconds() as f64;

if total_duration_ms > 0.0 {
if let Some(first_event) = self.replay_events.first() {
let start_time = first_event.timestamp;

// Draw events
for event in &self.replay_events {
let offset_ms = (event.timestamp - start_time).num_milliseconds() as f64;
let t = (offset_ms / total_duration_ms) as f32;
// Clamp t to [0.0, 1.0] to handle potential slight time skews
let t = t.clamp(0.0, 1.0);
let x = rect.min.x + t * rect.width();

let (color, height_fraction, y_offset) = match event.event_type {
EventType::Command { .. } => (COLOR_COMMAND, 0.8, 0.1),

Check failure

Code scanning / clippy

failed to resolve: use of undeclared type EventType Error

failed to resolve: use of undeclared type EventType
EventType::FileChange { .. } => (COLOR_FILE_CHANGE, 0.8, 0.1),

Check failure

Code scanning / clippy

failed to resolve: use of undeclared type EventType Error

failed to resolve: use of undeclared type EventType
EventType::TerminalState { .. } => (COLOR_TERMINAL, 0.4, 0.3),

Check failure

Code scanning / clippy

failed to resolve: use of undeclared type EventType Error

failed to resolve: use of undeclared type EventType
EventType::KeyPress { .. } => (COLOR_KEYPRESS, 0.2, 0.4),

Check failure

Code scanning / clippy

failed to resolve: use of undeclared type EventType Error

failed to resolve: use of undeclared type EventType
EventType::SessionMetadata { .. } => (COLOR_METADATA, 0.5, 0.25),

Check failure

Code scanning / clippy

failed to resolve: use of undeclared type EventType Error

failed to resolve: use of undeclared type EventType
};
Comment on lines +677 to +683
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The colors and layout properties for different event types are hardcoded as magic values within this match statement. To improve readability and maintainability, it's better to define these as named constants, perhaps at the top of the file or in a dedicated constants module. This makes it easier to see all visualization parameters in one place and change them consistently.


let y_start = rect.min.y + rect.height() * y_offset;
let y_end = y_start + rect.height() * height_fraction;

// Use thinner lines for keypresses to avoid clutter
let stroke_width = if matches!(event.event_type, EventType::KeyPress { .. }) {

Check failure

Code scanning / clippy

failed to resolve: use of undeclared type EventType Error

failed to resolve: use of undeclared type EventType
1.0
} else {
2.0
};

ui.painter().line_segment(
[egui::pos2(x, y_start), egui::pos2(x, y_end)],
egui::Stroke::new(stroke_width, color),
);
}
}

// Draw playback position indicator
let playback_t = (self.position_ms as f64 / total_duration_ms) as f32;
let playback_t = playback_t.clamp(0.0, 1.0);
let cursor_x = rect.min.x + playback_t * rect.width();

ui.painter().line_segment(
[egui::pos2(cursor_x, rect.min.y), egui::pos2(cursor_x, rect.max.y)],
egui::Stroke::new(2.0, egui::Color32::WHITE),
);

// Draw cursor triangle/head
ui.painter().circle_filled(
egui::pos2(cursor_x, rect.min.y),
4.0,
egui::Color32::WHITE,
);
} else {
ui.label("Session duration is zero, cannot display timeline.");
}

// Legend
ui.horizontal(|ui| {
ui.label("Legend:");
ui.colored_label(COLOR_COMMAND, "Command");
ui.colored_label(COLOR_FILE_CHANGE, "File Change");
ui.colored_label(COLOR_TERMINAL, "Terminal State");
});
Comment thread
nur-srijan marked this conversation as resolved.
});

} else {
Expand Down
16 changes: 11 additions & 5 deletions src/events.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use regex::Regex;
use sha2::{Digest, Sha256};
use serde::{Deserialize, Serialize};
use std::fs;
use std::io::Read;
use std::path::Path;
use uuid::Uuid;
use zeroize::Zeroize;
Expand Down Expand Up @@ -413,13 +414,18 @@ impl EventRecorder {
};
let mut reader = std::io::BufReader::new(file);
let mut hasher = Sha256::new();
match std::io::copy(&mut reader, &mut hasher) {
Ok(_) => {
let result = hasher.finalize();
Some(format!("{:x}", result))
let mut buffer = [0; 8192]; // 8KB buffer

loop {
match reader.read(&mut buffer) {
Ok(0) => break,
Ok(n) => hasher.update(&buffer[..n]),
Err(_) => return None,
}
Err(_) => None,
}

let result = hasher.finalize();
Some(format!("{:x}", result))
}
}

Expand Down
2 changes: 1 addition & 1 deletion src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -530,7 +530,7 @@
#[cfg(feature = "ai")]
async fn run_ai_summarize(session_id: &str, model: Option<&str>) -> Result<(), TimeLoopError> {
let model = model.unwrap_or("openrouter/auto");
let summary = timeloop_terminal::ai::summarize_session(session_id, model, None).await?;
let summary = timeloop_terminal::ai::summarize_session(session_id, model, api_key_opt).await?;

Check failure

Code scanning / clippy

cannot find value api_key_opt in this scope Error

cannot find value api_key_opt in this scope
println!("{}", summary);
Ok(())
}
Expand Down
101 changes: 34 additions & 67 deletions src/replay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ use crossterm::{
};
use std::io::Write;
use std::time::Duration;
use tokio::sync::mpsc;
use tokio::time::{sleep, sleep_until, Instant};
use tokio::time::{sleep, Instant};

pub struct ReplayEngine {
storage: Storage,
Expand Down Expand Up @@ -44,28 +43,6 @@ impl ReplayEngine {
let mut current_speed = if speed <= 0.0 { 1.0 } else { speed };
let mut paused = false;

// Spawn a background thread to handle input so we don't block the async executor
let (tx, mut rx) = mpsc::unbounded_channel();
// Use std::thread instead of spawn_blocking to ensure we have a dedicated thread
// for blocking poll operations without occupying a thread from the blocking pool forever.
std::thread::spawn(move || {
// Poll with a short timeout to check for exit conditions
while let Ok(available) = event::poll(Duration::from_millis(100)) {
if available {
if let Ok(evt) = event::read() {
if tx.send(evt).is_err() {
break; // Receiver dropped, stop thread
}
} else {
break;
}
} else if tx.is_closed() {
// Timeout, check if we should exit
break;
}
}
});

for (i, event) in events.iter().enumerate() {
// Calculate delay based on speed
let delay = if i > 0 {
Expand All @@ -78,50 +55,33 @@ impl ReplayEngine {

if delay > 0 {
let start = Instant::now();
let deadline = start + Duration::from_millis(delay);

loop {
let now = Instant::now();
if now >= deadline {
break;
}

while start.elapsed().as_millis() < delay as u128 {
// Handle interactive input during delay
let mut input_event = None;

if paused {
// If paused, we wait for input or a small timeout to emulate original behavior
// Original behavior: sleep 50ms, then check loop condition (which might exit if delay passed)
tokio::select! {
evt = rx.recv() => input_event = evt,
_ = sleep(Duration::from_millis(50)) => {}
}
} else {
// Wait for deadline or input
tokio::select! {
evt = rx.recv() => input_event = evt,
_ = sleep_until(deadline) => {}
}
}

if let Some(CEvent::Key(key)) = input_event {
match key.code {
KeyCode::Char(' ') => {
paused = !paused;
}
KeyCode::Char('+') => {
current_speed *= 2.0;
if event::poll(Duration::from_millis(50))? {
if let CEvent::Key(key) = event::read()? {
match key.code {
KeyCode::Char(' ') => {
paused = !paused;
}
KeyCode::Char('+') => {
current_speed *= 2.0;
}
KeyCode::Char('-') => {
current_speed = (current_speed / 2.0).max(0.25);
}
KeyCode::Char('q') => {
println!("\n⏹️ Quit replay");
return Ok(());
}
_ => {}
}
KeyCode::Char('-') => {
current_speed = (current_speed / 2.0).max(0.25);
}
KeyCode::Char('q') => {
println!("\n⏹️ Quit replay");
return Ok(());
}
_ => {}
}
}
if paused {
sleep(Duration::from_millis(50)).await;
continue;
}
sleep(Duration::from_millis(10)).await;
}
}

Expand Down Expand Up @@ -277,9 +237,16 @@ impl ReplayEngine {
Ok(())
}

pub fn get_events(&self) -> crate::Result<Vec<Event>> {
self.storage.get_events_for_session(&self.session_id)
}

pub fn get_session_summary(&self) -> crate::Result<ReplaySummary> {
let events = self.storage.get_events_for_session(&self.session_id)?;
Ok(Self::calculate_summary(&events))
}

pub fn calculate_summary(events: &[Event]) -> ReplaySummary {
let mut commands = 0;
let mut key_presses = 0;
let mut file_changes = 0;
Expand All @@ -289,7 +256,7 @@ impl ReplayEngine {
duration = last.timestamp - first.timestamp;
}

for event in &events {
for event in events {
match &event.event_type {
EventType::Command { .. } => commands += 1,
EventType::KeyPress { .. } => key_presses += 1,
Expand All @@ -298,13 +265,13 @@ impl ReplayEngine {
}
}

Ok(ReplaySummary {
ReplaySummary {
total_events: events.len(),
commands,
key_presses,
file_changes,
duration,
})
}
}
}

Expand Down
Loading
Loading