mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
feat(tui): add PR summary statusline items (#20892)
## Why? The Codex App already exposes branch and PR context in its branch-details UI. This brings the same context into the CLI footer as opt-in statusline items, so users can choose the extra signal without making the default footer busier. ## What? Add optional `pull-request-number` and `branch-changes` items to the configurable TUI status line. - `pull-request-number` shows the open PR for the current checkout and renders as a clickable terminal hyperlink when OSC 8 links are supported. - `branch-changes` shows committed additions/deletions against the repository default branch, or `No changes` when the branch has no committed diff. <img width="1257" height="261" alt="CleanShot 2026-05-03 at 20 44 15" src="https://github.com/user-attachments/assets/10b4380b-c3e9-4729-9ee1-3f742068fa47" /> ## Architecture This follows the same client/app-server split as the Codex App: the TUI owns presentation, caching, and optional rendering, while workspace-sensitive `git` and `gh` discovery runs through app-server. The new TUI-local `workspace_command` layer sends bounded, non-interactive `command/exec` requests to the active app-server. That makes the implementation remote-friendly: the TUI does not decide whether commands run in an embedded local workspace or a remote workspace, and it does not bypass app-server sandbox or permission policy. The branch summary logic stays internal to `codex-tui` because this PR only needs TUI statusline behavior. The command boundary is still isolated behind `WorkspaceCommandExecutor`, so the lookup code can be lifted or reused later without changing statusline rendering. ## How? - Add a TUI `WorkspaceCommandExecutor` abstraction backed by app-server `command/exec`. - Add branch summary probes for: - current branch name, - open PR metadata, - committed branch diff stats against the default branch. - Prefer remote-tracking default branch refs for diff stats, avoiding stale or absent local `main` branches. - Resolve PRs with `gh pr view` first, then fall back to commit-associated PR lookup across parent/fork repos. - Add `/statusline` picker entries, preview values, rendering, and OSC 8 clickable PR links. - Keep all probes best-effort so missing `git`, missing `gh`, auth failures, or non-git directories hide optional items instead of surfacing footer errors. ## Validation - `cargo test -p codex-tui branch_summary -- --nocapture` - Snapshot coverage for the `/statusline` preview/setup rendering paths - Hyperlink rendering coverage for clickable PR statusline cells
This commit is contained in:
@@ -349,11 +349,13 @@ use self::status_surfaces::TerminalTitleStatusKind;
|
||||
mod user_messages;
|
||||
use self::user_messages::PendingSteerCompareKey;
|
||||
use self::user_messages::UserMessageDisplay;
|
||||
pub(crate) use crate::branch_summary::StatusLineGitSummary;
|
||||
use crate::streaming::chunking::AdaptiveChunkingPolicy;
|
||||
use crate::streaming::commit_tick::CommitTickScope;
|
||||
use crate::streaming::commit_tick::run_commit_tick;
|
||||
use crate::streaming::controller::PlanStreamController;
|
||||
use crate::streaming::controller::StreamController;
|
||||
use crate::workspace_command::WorkspaceCommandRunner;
|
||||
|
||||
use chrono::Local;
|
||||
use codex_app_server_protocol::AskForApproval;
|
||||
@@ -554,6 +556,11 @@ pub(crate) struct ChatWidgetInit {
|
||||
pub(crate) config: Config,
|
||||
pub(crate) frame_requester: FrameRequester,
|
||||
pub(crate) app_event_tx: AppEventSender,
|
||||
/// App-server-backed runner used by status surfaces for workspace metadata probes.
|
||||
///
|
||||
/// Tests that do not exercise git status-line refreshes may leave this unset. Production TUI
|
||||
/// construction provides a runner for the active app-server session.
|
||||
pub(crate) workspace_command_runner: Option<WorkspaceCommandRunner>,
|
||||
pub(crate) initial_user_message: Option<UserMessage>,
|
||||
pub(crate) enhanced_keys_supported: bool,
|
||||
pub(crate) has_chatgpt_account: bool,
|
||||
@@ -972,6 +979,8 @@ pub(crate) struct ChatWidget {
|
||||
current_rollout_path: Option<PathBuf>,
|
||||
// Current working directory (if known)
|
||||
current_cwd: Option<PathBuf>,
|
||||
// App-server-backed command runner for status-line workspace metadata lookups.
|
||||
workspace_command_runner: Option<WorkspaceCommandRunner>,
|
||||
// Instruction source files loaded for the current session, supplied by app-server.
|
||||
instruction_source_paths: Vec<AbsolutePathBuf>,
|
||||
// Runtime network proxy bind addresses from SessionConfigured.
|
||||
@@ -1004,6 +1013,14 @@ pub(crate) struct ChatWidget {
|
||||
status_line_branch_pending: bool,
|
||||
// True once we've attempted a branch lookup for the current CWD.
|
||||
status_line_branch_lookup_complete: bool,
|
||||
// Cached PR and branch-change summary for the active status-line cwd.
|
||||
status_line_git_summary: Option<StatusLineGitSummary>,
|
||||
// CWD used to resolve the cached Git summary; change resets summary state.
|
||||
status_line_git_summary_cwd: Option<PathBuf>,
|
||||
// True while an async Git summary lookup is in flight.
|
||||
status_line_git_summary_pending: bool,
|
||||
// True once we've attempted a Git summary lookup for the current CWD.
|
||||
status_line_git_summary_lookup_complete: bool,
|
||||
// Current thread-goal status shown in the status line when plan mode is inactive.
|
||||
current_goal_status_indicator: Option<GoalStatusIndicator>,
|
||||
current_goal_status: Option<GoalStatusState>,
|
||||
@@ -1831,6 +1848,11 @@ impl ChatWidget {
|
||||
self.bottom_pane.set_status_line(status_line);
|
||||
}
|
||||
|
||||
/// Sets the terminal hyperlink target for the currently rendered footer status line.
|
||||
pub(crate) fn set_status_line_hyperlink(&mut self, url: Option<String>) {
|
||||
self.bottom_pane.set_status_line_hyperlink(url);
|
||||
}
|
||||
|
||||
/// Forwards the contextual active-agent label into the bottom-pane footer pipeline.
|
||||
///
|
||||
/// `ChatWidget` stays a pass-through here so `App` remains the owner of "which thread is the
|
||||
@@ -1929,6 +1951,22 @@ impl ChatWidget {
|
||||
self.refresh_status_surfaces();
|
||||
}
|
||||
|
||||
/// Stores async Git summary lookup results for the current status-line cwd.
|
||||
pub(crate) fn set_status_line_git_summary(
|
||||
&mut self,
|
||||
cwd: PathBuf,
|
||||
summary: StatusLineGitSummary,
|
||||
) {
|
||||
if self.status_line_git_summary_cwd.as_ref() != Some(&cwd) {
|
||||
self.status_line_git_summary_pending = false;
|
||||
return;
|
||||
}
|
||||
self.status_line_git_summary = Some(summary);
|
||||
self.status_line_git_summary_pending = false;
|
||||
self.status_line_git_summary_lookup_complete = true;
|
||||
self.refresh_status_surfaces();
|
||||
}
|
||||
|
||||
fn collect_runtime_metrics_delta(&mut self) {
|
||||
if let Some(delta) = self.session_telemetry.runtime_metrics_summary() {
|
||||
self.apply_runtime_metrics_delta(delta);
|
||||
@@ -2499,6 +2537,7 @@ impl ChatWidget {
|
||||
self.needs_final_message_separator = false;
|
||||
self.had_work_activity = false;
|
||||
self.request_status_line_branch_refresh();
|
||||
self.request_status_line_git_summary_refresh();
|
||||
}
|
||||
// Mark task stopped and request redraw now that all content is in history.
|
||||
self.pending_status_indicator_restore = false;
|
||||
@@ -2960,6 +2999,7 @@ impl ChatWidget {
|
||||
self.plan_stream_controller = None;
|
||||
self.pending_status_indicator_restore = false;
|
||||
self.request_status_line_branch_refresh();
|
||||
self.request_status_line_git_summary_refresh();
|
||||
self.maybe_show_pending_rate_limit_prompt();
|
||||
}
|
||||
|
||||
@@ -4768,6 +4808,7 @@ impl ChatWidget {
|
||||
config,
|
||||
frame_requester,
|
||||
app_event_tx,
|
||||
workspace_command_runner,
|
||||
initial_user_message,
|
||||
enhanced_keys_supported,
|
||||
has_chatgpt_account,
|
||||
@@ -4960,6 +5001,7 @@ impl ChatWidget {
|
||||
feedback,
|
||||
current_rollout_path: None,
|
||||
current_cwd,
|
||||
workspace_command_runner,
|
||||
instruction_source_paths: Vec::new(),
|
||||
session_network_proxy: None,
|
||||
status_line_invalid_items_warned,
|
||||
@@ -4973,6 +5015,10 @@ impl ChatWidget {
|
||||
status_line_branch_cwd: None,
|
||||
status_line_branch_pending: false,
|
||||
status_line_branch_lookup_complete: false,
|
||||
status_line_git_summary: None,
|
||||
status_line_git_summary_cwd: None,
|
||||
status_line_git_summary_pending: false,
|
||||
status_line_git_summary_lookup_complete: false,
|
||||
current_goal_status_indicator: None,
|
||||
current_goal_status: None,
|
||||
goal_status_active_turn_started_at: None,
|
||||
|
||||
Reference in New Issue
Block a user