feat(tui): live shell output during execution - show real-time progress#2048
feat(tui): live shell output during execution - show real-time progress#2048donglovejava wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
Code Review
This pull request introduces live incremental output for running execution cells in the TUI, allowing users to see progress before a command completes. This is implemented by adding a live_output field to ExecCell and polling the shell manager during the event loop. The review feedback identifies several optimization and correctness opportunities in the polling logic, such as avoiding redundant string allocations and clones, replacing fragile prefix-based matching with direct command comparisons, and preserving raw output by removing trim() calls.
| for (idx, cmd) in &running_execs { | ||
| let cmd_prefix: String = cmd.chars().take(100).collect(); | ||
| for job in &jobs { | ||
| if job.status == crate::tools::shell::ShellStatus::Running { | ||
| let job_prefix: String = job.command.chars().take(100).collect(); | ||
| if cmd_prefix == job_prefix { | ||
| let tail = if !job.stderr_tail.trim().is_empty() { | ||
| job.stderr_tail.trim() | ||
| } else { | ||
| job.stdout_tail.trim() | ||
| }; | ||
| if !tail.is_empty() { | ||
| if let Some(HistoryCell::Tool(ToolCell::Exec(ref mut ec))) = | ||
| active_ref.entry_mut(*idx) | ||
| { | ||
| let new_output = tail.to_string(); | ||
| if ec.live_output.as_deref() != Some(&new_output) { | ||
| ec.live_output = Some(new_output); | ||
| updated = true; | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
The matching and output selection logic can be significantly optimized and improved:
- Efficiency: The current code performs multiple string allocations (
collect::<String>()) inside a nested loop on every frame. Using direct string comparison (cmd == &job.command) avoids this overhead. Additionally, checking if the output changed before callingto_string()avoids redundant allocations. - Correctness: Matching by a 100-character prefix is fragile and can lead to incorrect matches if multiple similar commands are running simultaneously. Direct comparison is safer.
- Output Integrity: Using
trim()removes potentially significant whitespace, such as indentation or progress-indicating newlines. It is better to use the raw tail strings. - Priority: The current logic completely ignores
stdoutifstderrhas any content. While often acceptable, it can hide progress if the process emits warnings tostderrwhile continuing work onstdout.
for idx in &running_exec_indices {
let cmd = if let HistoryCell::Tool(ToolCell::Exec(ec)) = &active_ref.entries()[*idx] { &ec.command } else { continue };
for job in &jobs {
if job.status == crate::tools::shell::ShellStatus::Running && &job.command == cmd {
let tail = if !job.stderr_tail.is_empty() {
&job.stderr_tail
} else {
&job.stdout_tail
};
if !tail.is_empty() {
if let Some(HistoryCell::Tool(ToolCell::Exec(ref mut ec))) = active_ref.entry_mut(*idx) {
if ec.live_output.as_deref() != Some(tail) {
ec.live_output = Some(tail.to_string());
updated = true;
}
}
}
}
}
}| let mut running_execs: Vec<(usize, String)> = Vec::new(); | ||
| for (i, entry) in active.entries().iter().enumerate() { | ||
| if let HistoryCell::Tool(ToolCell::Exec(ExecCell { | ||
| command, | ||
| status: crate::tui::history::ToolStatus::Running, | ||
| .. | ||
| })) = entry | ||
| { | ||
| running_execs.push((i, command.clone())); | ||
| } | ||
| } |
There was a problem hiding this comment.
Cloning the command string for every running execution cell on every frame is inefficient. You can store just the indices and retrieve the command from the active_cell when needed to avoid unnecessary allocations.
let mut running_exec_indices: Vec<usize> = Vec::new();
for (i, entry) in active.entries().iter().enumerate() {
if let HistoryCell::Tool(ToolCell::Exec(ExecCell {
status: crate::tui::history::ToolStatus::Running,
..
})) = entry
{
running_exec_indices.push(i);
}
}
Summary
Add real-time incremental output display for shell execution commands. The TUI now displays shell output while the command is still running, instead of hiding all output until completion.
Changes
ExecCell: Addedlive_output: Option<String>field to store incremental output during executionhistory.rs: Modified render logic to showlive_outputwhenoutputis not yet available (priority: final output > live output > hints)app.rs: Addedpoll_shell_progress()method that pollsShellManagerduring idle frames and updates matchingExecCellentriesui.rs: Wiredpoll_shell_progress()into the event loop aftertick_quit_armed()live_output: Noneto 8 filesHow it works
ShellManager.list_jobs()for running shell processesExecCell.live_outputFiles changed