feat(tui): route /diff through workspace commands (#21001)

Stacked on #20892.

## Why

#20892 adds the TUI workspace command abstraction so branch status
metadata can run through app-server instead of assuming the CLI process
has the active workspace locally. `/diff` still used direct local
process execution, which means remote app-server sessions could compute
the diff against the wrong machine or fail to see the active workspace
at all.

This PR moves `/diff` onto that same app-server-backed command path so
Git runs wherever the active workspace lives.

## What Changed

- Route `/diff` through the TUI `WorkspaceCommandExecutor` using the
active chat cwd.
- Replace direct `tokio::process::Command` usage in `get_git_diff` with
argv-based workspace command requests.
- Preserve the existing `/diff` behavior: tracked diff output, untracked
file diffs, treating Git diff exit code `1` as success, and showing the
existing non-git-repository message.
- Extend `WorkspaceCommand` with caller-set timeouts and an explicit
uncapped-output opt-out. Metadata probes remain capped by default;
`/diff` opts out because its full output is the user-visible payload.

## How to Test

Manual reviewer path:

1. Start the Codex TUI from a Git worktree with one tracked file change
and one untracked file.
2. Run `/diff`.
3. Confirm the rendered diff includes both the tracked diff and the
untracked file diff.
4. Start the TUI outside a Git worktree, or switch to a non-git cwd,
then run `/diff`.
5. Confirm it shows the existing `/diff` not-inside-a-git-repository
message.

Targeted tests run:

- `cargo test -p codex-tui get_git_diff -- --nocapture`
- `cargo test -p codex-tui branch_summary -- --nocapture`
- `cargo test -p codex-tui`
This commit is contained in:
Felipe Coury
2026-05-05 17:09:25 -03:00
committed by GitHub
Unverified
parent 9e0c191c13
commit 52fbbe7cdd
3 changed files with 322 additions and 77 deletions
+24 -8
View File
@@ -1,14 +1,14 @@
//! App-server-backed workspace command execution for TUI-owned background lookups.
//!
//! This module is the TUI boundary for short, non-interactive commands that need to run wherever
//! This module is the TUI boundary for non-interactive commands that need to run wherever
//! the active workspace lives. Callers describe a command in terms of argv, cwd, environment
//! overrides, timeout, and output cap; the runner translates that request to app-server
//! `command/exec`. Keeping this as a TUI-local abstraction lets status surfaces avoid knowing
//! whether the current app-server is embedded or remote.
//!
//! Commands sent through this path are best-effort metadata probes. They should not prompt for
//! stdin, should tolerate failure by omitting optional UI, and should keep output bounded so a
//! status-line refresh cannot grow into an unbounded background process.
//! Commands sent through this path should not prompt for stdin. Most callers should keep output
//! bounded so metadata refreshes cannot grow into unbounded background processes; callers that own a
//! full user-visible payload, such as `/diff`, can explicitly opt out of output capping.
use std::collections::HashMap;
use std::future::Future;
@@ -45,17 +45,20 @@ pub(crate) struct WorkspaceCommand {
pub(crate) timeout: Duration,
/// Maximum captured stdout/stderr bytes returned by app-server.
pub(crate) output_bytes_cap: usize,
/// Whether app-server should return uncapped stdout/stderr.
pub(crate) disable_output_cap: bool,
}
impl WorkspaceCommand {
/// Creates a workspace command with conservative defaults for status-style metadata probes.
/// Creates a workspace command with conservative defaults for metadata probes.
pub(crate) fn new(argv: impl IntoIterator<Item = impl Into<String>>) -> Self {
Self {
argv: argv.into_iter().map(Into::into).collect(),
cwd: None,
env: HashMap::new(),
timeout: Duration::from_secs(5),
timeout: Duration::from_secs(/*secs*/ 5),
output_bytes_cap: 64 * 1024,
disable_output_cap: false,
}
}
@@ -70,6 +73,18 @@ impl WorkspaceCommand {
self.env.insert(key.into(), Some(value.into()));
self
}
/// Sets the maximum wall-clock duration before app-server cancels the command.
pub(crate) fn timeout(mut self, timeout: Duration) -> Self {
self.timeout = timeout;
self
}
/// Requests uncapped stdout/stderr capture from app-server.
pub(crate) fn disable_output_cap(mut self) -> Self {
self.disable_output_cap = true;
self
}
}
/// Captured result from a completed workspace command.
@@ -176,8 +191,9 @@ impl WorkspaceCommandExecutor for AppServerWorkspaceCommandRunner {
tty: false,
stream_stdin: false,
stream_stdout_stderr: false,
output_bytes_cap: Some(command.output_bytes_cap),
disable_output_cap: false,
output_bytes_cap: (!command.disable_output_cap)
.then_some(command.output_bytes_cap),
disable_output_cap: command.disable_output_cap,
disable_timeout: false,
timeout_ms: Some(timeout_ms),
cwd: command.cwd,