Skip to content

feat(tui): live shell output during execution - show real-time progress#2048

Open
donglovejava wants to merge 1 commit into
Hmbown:mainfrom
donglovejava:feat/shell-live-progress
Open

feat(tui): live shell output during execution - show real-time progress#2048
donglovejava wants to merge 1 commit into
Hmbown:mainfrom
donglovejava:feat/shell-live-progress

Conversation

@donglovejava
Copy link
Copy Markdown

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: Added live_output: Option<String> field to store incremental output during execution
  • history.rs: Modified render logic to show live_output when output is not yet available (priority: final output > live output > hints)
  • app.rs: Added poll_shell_progress() method that polls ShellManager during idle frames and updates matching ExecCell entries
  • ui.rs: Wired poll_shell_progress() into the event loop after tick_quit_armed()
  • All ExecCell construction sites: Added live_output: None to 8 files

How it works

  1. During idle frames, the TUI polls ShellManager.list_jobs() for running shell processes
  2. Running exec cells are matched to shell jobs by command prefix
  3. Live output (tail of stdout/stderr) is written to ExecCell.live_output
  4. The renderer displays it in the transcript immediately

Files changed

crates/tui/src/tui/active_cell.rs  |  1 +
crates/tui/src/tui/app.rs          | 69 +++++++++++++++++++++++++++++
crates/tui/src/tui/history.rs      | 11 ++++
crates/tui/src/tui/sidebar.rs      |  5 ++
crates/tui/src/tui/tool_routing.rs |  2 +
crates/tui/src/tui/transcript.rs   |  1 +
crates/tui/src/tui/ui.rs           |  2 +
crates/tui/src/tui/ui/tests.rs     |  3 +

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

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

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.

Comment thread crates/tui/src/tui/app.rs
Comment on lines +2554 to +2579
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;
}
}
}
}
}
}
}
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

The matching and output selection logic can be significantly optimized and improved:

  1. 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 calling to_string() avoids redundant allocations.
  2. 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.
  3. Output Integrity: Using trim() removes potentially significant whitespace, such as indentation or progress-indicating newlines. It is better to use the raw tail strings.
  4. Priority: The current logic completely ignores stdout if stderr has any content. While often acceptable, it can hide progress if the process emits warnings to stderr while continuing work on stdout.
        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;
                            }
                        }
                    }
                }
            }
        }

Comment thread crates/tui/src/tui/app.rs
Comment on lines +2531 to +2541
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()));
}
}
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

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);
            }
        }

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant