mirror of
https://github.com/openai/codex.git
synced 2026-04-28 10:21:06 +03:00
5.2 KiB
5.2 KiB
Asynchronous Diff UI: DOs and DON’Ts
DOs
- Use tokio::spawn for work: Offload long-running tasks to keep the UI responsive.
let tx = self.app_event_tx.clone();
tokio::spawn(async move {
let text = match get_git_diff().await {
Ok((true, diff)) => diff,
Ok((false, _)) => "`/diff` — _not inside a git repository_".to_string(),
Err(e) => format!("Failed to compute diff: {e}"),
};
let _ = tx.send(AppEvent::DiffResult(text));
});
- Make helpers async: Switch to
tokio::process::Commandand.awaitprocess I/O.
use std::{io, process::Stdio};
use tokio::process::Command;
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().await?;
if output.status.success() || output.status.code() == Some(1) {
Ok(String::from_utf8_lossy(&output.stdout).into_owned())
} else {
Err(io::Error::new(io::ErrorKind::Other, format!("git {:?} failed", args)))
}
}
- Report results via AppEvent: Define and handle a dedicated event for the async result.
// app_event.rs
pub(crate) enum AppEvent {
// ...
DiffResult(String),
}
// app.rs
AppEvent::DiffResult(text) => {
if let AppState::Chat { widget } = &mut self.app_state {
widget.add_diff_output(text);
}
}
- Show a progress indicator: Set a running state and update status text while the task runs.
// chatwidget.rs
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.bottom_pane.set_task_running(false);
self.add_to_history(&history_cell::new_diff_output(diff_output));
self.mark_needs_redraw();
}
- Gate status updates through the view: Only update when the status view is active.
// bottom_pane/mod.rs
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();
}
}
- Parallelize independent subprocesses: Use
tokio::join!andJoinSetfor concurrency.
// run tracked diff + list untracked in parallel
let (tracked_diff_res, untracked_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_list = untracked_res?;
// diff untracked files concurrently
let null_path = "/dev/null".to_string();
let mut join_set = tokio::task::JoinSet::new();
for f in untracked_list.lines().map(str::trim).filter(|s| !s.is_empty()) {
let f = f.to_string();
let null_path = null_path.clone();
join_set.spawn(async move {
run_git_capture_diff(&["diff","--color","--no-index","--",&null_path,&f]).await
});
}
let mut untracked_diff = String::new();
while let Some(res) = join_set.join_next().await {
match res {
Ok(Ok(diff)) => untracked_diff.push_str(&diff),
Ok(Err(e)) if e.kind() == io::ErrorKind::NotFound => {},
Ok(Err(e)) => return Err(e),
Err(_) => {}
}
}
- Check repo state first: Short-circuit gracefully when not inside a Git repo.
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().await;
match status {
Ok(s) if s.success() => Ok(true),
Ok(_) => Ok(false),
Err(e) => Err(e),
}
}
DON’Ts
- Don’t block the UI thread: Avoid synchronous process I/O in the event loop.
// Bad: blocks the reactor and UI
let out = std::process::Command::new("git").arg("diff").output().unwrap();
// Good: async process
let out = tokio::process::Command::new("git").arg("diff").output().await?;
- Don’t spawn std threads in Tokio contexts: Prefer
tokio::spawnto integrate with the runtime.
// Bad
std::thread::spawn(move || { /* ... */ });
// Good
tokio::spawn(async move { /* ... */ });
- Don’t mutate UI from background tasks: Send an event; handle all UI updates on the main thread.
// Bad
tokio::spawn(async move {
widget.add_diff_output("...".to_string()); // UI from background
});
// Good
tokio::spawn(async move {
let _ = tx.send(AppEvent::DiffResult("...".to_string()));
});
- Don’t forget to end the progress state: Always clear
task_runningwhen the task completes.
// Ensure this runs on completion paths
self.bottom_pane.set_task_running(false);
- Don’t ignore Git’s special exit codes: Treat diff exit code 1 as success; surface real failures.
// Correct handling
if output.status.success() || output.status.code() == Some(1) {
// differences present or clean
} else {
return Err(io::Error::new(io::ErrorKind::Other, format!("git {:?} failed", args)));
}