mirror of
https://github.com/openai/codex.git
synced 2026-04-28 08:34:54 +00:00
338 lines
13 KiB
Markdown
338 lines
13 KiB
Markdown
# PR #2245: Show progress indicator for /diff command
|
||
|
||
- URL: https://github.com/openai/codex/pull/2245
|
||
- Author: aibrahim-oai
|
||
- Created: 2025-08-12 23:53:28 UTC
|
||
- Updated: 2025-08-15 22:32:47 UTC
|
||
- Changes: +89/-47, Files changed: 7, Commits: 13
|
||
|
||
## Description
|
||
|
||
## Summary
|
||
- Show a temporary Working on diff state in the bottom pan
|
||
- Add `DiffResult` app event and dispatch git diff asynchronously
|
||
|
||
## Testing
|
||
- `just fmt`
|
||
- `just fix` *(fails: `let` expressions in this position are unstable)*
|
||
- `cargo test --all-features` *(fails: `let` expressions in this position are unstable)*
|
||
|
||
------
|
||
https://chatgpt.com/codex/tasks/task_i_689a839f32b88321840a893551d5fbef
|
||
|
||
## Full Diff
|
||
|
||
```diff
|
||
diff --git a/codex-rs/tui/src/app.rs b/codex-rs/tui/src/app.rs
|
||
index ee054e037a..3d12ad5427 100644
|
||
--- a/codex-rs/tui/src/app.rs
|
||
+++ b/codex-rs/tui/src/app.rs
|
||
@@ -345,6 +345,11 @@ impl App<'_> {
|
||
AppState::Chat { widget } => widget.submit_op(op),
|
||
AppState::Onboarding { .. } => {}
|
||
},
|
||
+ AppEvent::DiffResult(text) => {
|
||
+ if let AppState::Chat { widget } = &mut self.app_state {
|
||
+ widget.add_diff_output(text);
|
||
+ }
|
||
+ }
|
||
AppEvent::DispatchCommand(command) => match command {
|
||
SlashCommand::New => {
|
||
// User accepted – switch to chat view.
|
||
@@ -382,25 +387,24 @@ impl App<'_> {
|
||
break;
|
||
}
|
||
SlashCommand::Diff => {
|
||
- let (is_git_repo, diff_text) = match get_git_diff() {
|
||
- Ok(v) => v,
|
||
- Err(e) => {
|
||
- let msg = format!("Failed to compute diff: {e}");
|
||
- if let AppState::Chat { widget } = &mut self.app_state {
|
||
- widget.add_diff_output(msg);
|
||
- }
|
||
- continue;
|
||
- }
|
||
- };
|
||
-
|
||
if let AppState::Chat { widget } = &mut self.app_state {
|
||
- let text = if is_git_repo {
|
||
- diff_text
|
||
- } else {
|
||
- "`/diff` — _not inside a git repository_".to_string()
|
||
- };
|
||
- widget.add_diff_output(text);
|
||
+ widget.add_diff_in_progress();
|
||
}
|
||
+
|
||
+ let tx = self.app_event_tx.clone();
|
||
+ tokio::spawn(async move {
|
||
+ let text = match get_git_diff().await {
|
||
+ Ok((is_git_repo, diff_text)) => {
|
||
+ if is_git_repo {
|
||
+ diff_text
|
||
+ } else {
|
||
+ "`/diff` — _not inside a git repository_".to_string()
|
||
+ }
|
||
+ }
|
||
+ Err(e) => format!("Failed to compute diff: {e}"),
|
||
+ };
|
||
+ tx.send(AppEvent::DiffResult(text));
|
||
+ });
|
||
}
|
||
SlashCommand::Mention => {
|
||
if let AppState::Chat { widget } = &mut self.app_state {
|
||
diff --git a/codex-rs/tui/src/app_event.rs b/codex-rs/tui/src/app_event.rs
|
||
index 6b9999848c..1afffd756a 100644
|
||
--- a/codex-rs/tui/src/app_event.rs
|
||
+++ b/codex-rs/tui/src/app_event.rs
|
||
@@ -51,6 +51,9 @@ pub(crate) enum AppEvent {
|
||
matches: Vec<FileMatch>,
|
||
},
|
||
|
||
+ /// Result of computing a `/diff` command.
|
||
+ DiffResult(String),
|
||
+
|
||
InsertHistory(Vec<Line<'static>>),
|
||
|
||
StartCommitAnimation,
|
||
diff --git a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs
|
||
index c86dad3183..31a32140cb 100644
|
||
--- a/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs
|
||
+++ b/codex-rs/tui/src/bottom_pane/bottom_pane_view.rs
|
||
@@ -41,4 +41,8 @@ pub(crate) trait BottomPaneView<'a> {
|
||
) -> Option<ApprovalRequest> {
|
||
Some(request)
|
||
}
|
||
+
|
||
+ /// Optional hook for views that expose a live status line. Views that do not
|
||
+ /// support this can ignore the call.
|
||
+ fn update_status_text(&mut self, _text: String) {}
|
||
}
|
||
diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs
|
||
index b2da8d28a6..85ed59de3c 100644
|
||
--- a/codex-rs/tui/src/bottom_pane/mod.rs
|
||
+++ b/codex-rs/tui/src/bottom_pane/mod.rs
|
||
@@ -210,6 +210,19 @@ impl BottomPane<'_> {
|
||
}
|
||
}
|
||
|
||
+ /// Update the live status text shown while a task is running.
|
||
+ /// If a modal view is active (i.e., not the status indicator), this is a no‑op.
|
||
+ pub(crate) fn update_status_text(&mut self, text: String) {
|
||
+ if !self.is_task_running || !self.status_view_active {
|
||
+ return;
|
||
+ }
|
||
+ if let Some(mut view) = self.active_view.take() {
|
||
+ view.update_status_text(text);
|
||
+ self.active_view = Some(view);
|
||
+ self.request_redraw();
|
||
+ }
|
||
+ }
|
||
+
|
||
pub(crate) fn composer_is_empty(&self) -> bool {
|
||
self.composer.is_empty()
|
||
}
|
||
diff --git a/codex-rs/tui/src/bottom_pane/status_indicator_view.rs b/codex-rs/tui/src/bottom_pane/status_indicator_view.rs
|
||
index b0f64a972e..ccfd240b58 100644
|
||
--- a/codex-rs/tui/src/bottom_pane/status_indicator_view.rs
|
||
+++ b/codex-rs/tui/src/bottom_pane/status_indicator_view.rs
|
||
@@ -43,4 +43,8 @@ impl BottomPaneView<'_> for StatusIndicatorView {
|
||
self.view.interrupt();
|
||
}
|
||
}
|
||
+
|
||
+ fn update_status_text(&mut self, text: String) {
|
||
+ self.update_text(text);
|
||
+ }
|
||
}
|
||
diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs
|
||
index 637d50bd33..b7bd380ba5 100644
|
||
--- a/codex-rs/tui/src/chatwidget.rs
|
||
+++ b/codex-rs/tui/src/chatwidget.rs
|
||
@@ -659,8 +659,17 @@ impl ChatWidget<'_> {
|
||
self.app_event_tx.send(AppEvent::RequestRedraw);
|
||
}
|
||
|
||
+ pub(crate) fn add_diff_in_progress(&mut self) {
|
||
+ self.bottom_pane.set_task_running(true);
|
||
+ self.bottom_pane
|
||
+ .update_status_text("computing diff".to_string());
|
||
+ self.request_redraw();
|
||
+ }
|
||
+
|
||
pub(crate) fn add_diff_output(&mut self, diff_output: String) {
|
||
- self.add_to_history(&history_cell::new_diff_output(diff_output.clone()));
|
||
+ self.bottom_pane.set_task_running(false);
|
||
+ self.add_to_history(&history_cell::new_diff_output(diff_output));
|
||
+ self.mark_needs_redraw();
|
||
}
|
||
|
||
pub(crate) fn add_status_output(&mut self) {
|
||
diff --git a/codex-rs/tui/src/get_git_diff.rs b/codex-rs/tui/src/get_git_diff.rs
|
||
index e6ba9f4d83..78ab53d92f 100644
|
||
--- a/codex-rs/tui/src/get_git_diff.rs
|
||
+++ b/codex-rs/tui/src/get_git_diff.rs
|
||
@@ -7,24 +7,26 @@
|
||
|
||
use std::io;
|
||
use std::path::Path;
|
||
-use std::process::Command;
|
||
use std::process::Stdio;
|
||
+use tokio::process::Command;
|
||
|
||
/// Return value of [`get_git_diff`].
|
||
///
|
||
/// * `bool` – Whether the current working directory is inside a Git repo.
|
||
/// * `String` – The concatenated diff (may be empty).
|
||
-pub(crate) fn get_git_diff() -> io::Result<(bool, String)> {
|
||
+pub(crate) async fn get_git_diff() -> io::Result<(bool, String)> {
|
||
// First check if we are inside a Git repository.
|
||
- if !inside_git_repo()? {
|
||
+ if !inside_git_repo().await? {
|
||
return Ok((false, String::new()));
|
||
}
|
||
|
||
- // 1. Diff for tracked files.
|
||
- let tracked_diff = run_git_capture_diff(&["diff", "--color"])?;
|
||
-
|
||
- // 2. Determine untracked files.
|
||
- let untracked_output = run_git_capture_stdout(&["ls-files", "--others", "--exclude-standard"])?;
|
||
+ // Run tracked diff and untracked file listing in parallel.
|
||
+ let (tracked_diff_res, untracked_output_res) = tokio::join!(
|
||
+ run_git_capture_diff(&["diff", "--color"]),
|
||
+ run_git_capture_stdout(&["ls-files", "--others", "--exclude-standard"]),
|
||
+ );
|
||
+ let tracked_diff = tracked_diff_res?;
|
||
+ let untracked_output = untracked_output_res?;
|
||
|
||
let mut untracked_diff = String::new();
|
||
let null_device: &Path = if cfg!(windows) {
|
||
@@ -33,26 +35,26 @@ pub(crate) fn get_git_diff() -> io::Result<(bool, String)> {
|
||
Path::new("/dev/null")
|
||
};
|
||
|
||
+ let null_path = null_device.to_str().unwrap_or("/dev/null").to_string();
|
||
+ let mut join_set: tokio::task::JoinSet<io::Result<String>> = tokio::task::JoinSet::new();
|
||
for file in untracked_output
|
||
.split('\n')
|
||
.map(str::trim)
|
||
.filter(|s| !s.is_empty())
|
||
{
|
||
- // Use `git diff --no-index` to generate a diff against the null device.
|
||
- let args = [
|
||
- "diff",
|
||
- "--color",
|
||
- "--no-index",
|
||
- "--",
|
||
- null_device.to_str().unwrap_or("/dev/null"),
|
||
- file,
|
||
- ];
|
||
-
|
||
- match run_git_capture_diff(&args) {
|
||
- Ok(diff) => untracked_diff.push_str(&diff),
|
||
- // If the file disappeared between ls-files and diff we ignore the error.
|
||
- Err(err) if err.kind() == io::ErrorKind::NotFound => {}
|
||
- Err(err) => return Err(err),
|
||
+ let null_path = null_path.clone();
|
||
+ let file = file.to_string();
|
||
+ join_set.spawn(async move {
|
||
+ let args = ["diff", "--color", "--no-index", "--", &null_path, &file];
|
||
+ run_git_capture_diff(&args).await
|
||
+ });
|
||
+ }
|
||
+ while let Some(res) = join_set.join_next().await {
|
||
+ match res {
|
||
+ Ok(Ok(diff)) => untracked_diff.push_str(&diff),
|
||
+ Ok(Err(err)) if err.kind() == io::ErrorKind::NotFound => {}
|
||
+ Ok(Err(err)) => return Err(err),
|
||
+ Err(_) => {}
|
||
}
|
||
}
|
||
|
||
@@ -61,12 +63,13 @@ pub(crate) fn get_git_diff() -> io::Result<(bool, String)> {
|
||
|
||
/// Helper that executes `git` with the given `args` and returns `stdout` as a
|
||
/// UTF-8 string. Any non-zero exit status is considered an *error*.
|
||
-fn run_git_capture_stdout(args: &[&str]) -> io::Result<String> {
|
||
+async fn run_git_capture_stdout(args: &[&str]) -> io::Result<String> {
|
||
let output = Command::new("git")
|
||
.args(args)
|
||
.stdout(Stdio::piped())
|
||
.stderr(Stdio::null())
|
||
- .output()?;
|
||
+ .output()
|
||
+ .await?;
|
||
|
||
if output.status.success() {
|
||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||
@@ -80,12 +83,13 @@ fn run_git_capture_stdout(args: &[&str]) -> io::Result<String> {
|
||
|
||
/// Like [`run_git_capture_stdout`] but treats exit status 1 as success and
|
||
/// returns stdout. Git returns 1 for diffs when differences are present.
|
||
-fn run_git_capture_diff(args: &[&str]) -> io::Result<String> {
|
||
+async fn run_git_capture_diff(args: &[&str]) -> io::Result<String> {
|
||
let output = Command::new("git")
|
||
.args(args)
|
||
.stdout(Stdio::piped())
|
||
.stderr(Stdio::null())
|
||
- .output()?;
|
||
+ .output()
|
||
+ .await?;
|
||
|
||
if output.status.success() || output.status.code() == Some(1) {
|
||
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
|
||
@@ -98,12 +102,13 @@ fn run_git_capture_diff(args: &[&str]) -> io::Result<String> {
|
||
}
|
||
|
||
/// Determine if the current directory is inside a Git repository.
|
||
-fn inside_git_repo() -> io::Result<bool> {
|
||
+async fn inside_git_repo() -> io::Result<bool> {
|
||
let status = Command::new("git")
|
||
.args(["rev-parse", "--is-inside-work-tree"])
|
||
.stdout(Stdio::null())
|
||
.stderr(Stdio::null())
|
||
- .status();
|
||
+ .status()
|
||
+ .await;
|
||
|
||
match status {
|
||
Ok(s) if s.success() => Ok(true),
|
||
```
|
||
|
||
## Review Comments
|
||
|
||
### codex-rs/tui/src/app.rs
|
||
|
||
- Created: 2025-08-13 15:10:54 UTC | Link: https://github.com/openai/codex/pull/2245#discussion_r2273777646
|
||
|
||
```diff
|
||
@@ -346,25 +351,24 @@ impl App<'_> {
|
||
break;
|
||
}
|
||
SlashCommand::Diff => {
|
||
- let (is_git_repo, diff_text) = match get_git_diff() {
|
||
- Ok(v) => v,
|
||
- Err(e) => {
|
||
- let msg = format!("Failed to compute diff: {e}");
|
||
- if let AppState::Chat { widget } = &mut self.app_state {
|
||
- widget.add_diff_output(msg);
|
||
- }
|
||
- continue;
|
||
- }
|
||
- };
|
||
-
|
||
if let AppState::Chat { widget } = &mut self.app_state {
|
||
- let text = if is_git_repo {
|
||
- diff_text
|
||
- } else {
|
||
- "`/diff` — _not inside a git repository_".to_string()
|
||
- };
|
||
- widget.add_diff_output(text);
|
||
+ widget.add_diff_in_progress();
|
||
}
|
||
+
|
||
+ let tx = self.app_event_tx.clone();
|
||
+ thread::spawn(move || {
|
||
```
|
||
|
||
> We can `tokio::spawn()` and make `get_git_diff()` async then, right? |