Files
codex/codex-rs/tui/src/chatwidget/review_popups.rs
T
Eric Traut 3c3e18c222 Refactor chatwidget orchestration into modules (phase 5) (#22537)
## Why

`chatwidget.rs` is still carrying too many unrelated responsibilities in
one file. #22269 started a five-phase cleanup to move coherent behavior
domains into focused modules while keeping `chatwidget.rs` as the
composition layer. #22407 completed phase 2 by extracting input and
submission flow, #22433 completed phase 3 by extracting protocol,
replay, streaming, and tool lifecycle handling, and #22518 completed
phase 4 by extracting settings, popups, and status surfaces.

This PR is phase 5. It cleans up the remaining constructor and
orchestration code now that the larger behavior domains have moved out,
leaving `chatwidget.rs` much closer to the composition layer the cleanup
was aiming for. This is once again a mechanical movement of existing
functions. No functional changes.

## What Changed

- Added focused modules for widget construction and initial wiring,
session configuration flow, key/composer interaction routing, review
popup orchestration, desktop notification coalescing, and render
composition.
- Moved the remaining constructor, session setup, interaction,
notification, review picker, and rendering helpers out of
`codex-rs/tui/src/chatwidget.rs`.
- Preserved the existing startup/session behavior, keyboard handling,
review picker flow, notification priority behavior, and render
composition while shrinking the central widget module substantially.
- Left `codex-rs/tui/src/chatwidget.rs` as the registration and
composition surface for the extracted behavior modules.

## Cleanup Phases

The five-phase cleanup plan from #22269 is:

1. Phase 1: mechanical helper and state moves. Completed in #22269.
2. Phase 2: extract input and submission flow, including queued user
messages, shell prompt submission, pending steer restoration, and thread
input snapshot/restore behavior. Completed in #22407.
3. Phase 3: extract protocol, replay, streaming, and tool lifecycle
handling, while preserving active-cell grouping, transcript
invalidation, interrupt deferral, and final-message separator behavior.
Completed in #22433.
4. Phase 4: extract settings, popups, and status surfaces, including
model/reasoning/collaboration/personality popups, permission prompts,
rate-limit UI, and connectors helpers. Completed in #22518.
5. Phase 5: clean up the remaining constructor and orchestration code
once the larger behavior domains have moved out, leaving `chatwidget.rs`
as the composition layer. This PR.

## Verification

- `cargo check -p codex-tui`
- `cargo test -p codex-tui chatwidget::tests::popups_and_settings`
- `cargo test -p codex-tui chatwidget::tests::plan_mode`
- `cargo test -p codex-tui chatwidget::tests::review_mode`
- `cargo test -p codex-tui chatwidget::tests::status_and_layout`

`cargo test -p codex-tui` also compiles and begins running, but aborts
in the unchanged app-side test
`app::tests::discard_side_thread_keeps_local_state_when_server_close_fails`
with the same reproducible stack overflow noted in phase 4.
2026-05-13 15:40:53 -07:00

183 lines
6.3 KiB
Rust

//! Review preset selection and custom review prompt surfaces.
use super::*;
impl ChatWidget {
pub(crate) fn open_review_popup(&mut self) {
let mut items: Vec<SelectionItem> = Vec::new();
items.push(SelectionItem {
name: "Review against a base branch".to_string(),
description: Some("(PR Style)".into()),
actions: vec![Box::new({
let cwd = self.config.cwd.to_path_buf();
move |tx| {
tx.send(AppEvent::OpenReviewBranchPicker(cwd.clone()));
}
})],
dismiss_on_select: false,
dismiss_parent_on_child_accept: true,
..Default::default()
});
items.push(SelectionItem {
name: "Review uncommitted changes".to_string(),
actions: vec![Box::new(move |tx: &AppEventSender| {
tx.review(ReviewTarget::UncommittedChanges);
})],
dismiss_on_select: true,
..Default::default()
});
items.push(SelectionItem {
name: "Review a commit".to_string(),
actions: vec![Box::new({
let cwd = self.config.cwd.to_path_buf();
move |tx| {
tx.send(AppEvent::OpenReviewCommitPicker(cwd.clone()));
}
})],
dismiss_on_select: false,
dismiss_parent_on_child_accept: true,
..Default::default()
});
items.push(SelectionItem {
name: "Custom review instructions".to_string(),
actions: vec![Box::new(move |tx| {
tx.send(AppEvent::OpenReviewCustomPrompt);
})],
dismiss_on_select: false,
dismiss_parent_on_child_accept: true,
..Default::default()
});
self.bottom_pane.show_selection_view(SelectionViewParams {
title: Some("Select a review preset".into()),
footer_hint: Some(standard_popup_hint_line()),
items,
..Default::default()
});
}
pub(crate) async fn show_review_branch_picker(&mut self, cwd: &Path) {
let branches = local_git_branches(cwd).await;
let current_branch = current_branch_name(cwd)
.await
.unwrap_or_else(|| "(detached HEAD)".to_string());
let mut items: Vec<SelectionItem> = Vec::with_capacity(branches.len());
for option in branches {
let branch = option.clone();
items.push(SelectionItem {
name: format!("{current_branch} -> {branch}"),
actions: vec![Box::new(move |tx3: &AppEventSender| {
tx3.review(ReviewTarget::BaseBranch {
branch: branch.clone(),
});
})],
dismiss_on_select: true,
search_value: Some(option),
..Default::default()
});
}
self.bottom_pane.show_selection_view(SelectionViewParams {
title: Some("Select a base branch".to_string()),
footer_hint: Some(standard_popup_hint_line()),
items,
is_searchable: true,
search_placeholder: Some("Type to search branches".to_string()),
..Default::default()
});
}
pub(crate) async fn show_review_commit_picker(&mut self, cwd: &Path) {
let commits = recent_commits(cwd, /*limit*/ 100).await;
let mut items: Vec<SelectionItem> = Vec::with_capacity(commits.len());
for entry in commits {
let subject = entry.subject.clone();
let sha = entry.sha.clone();
let search_val = format!("{subject} {sha}");
items.push(SelectionItem {
name: subject.clone(),
actions: vec![Box::new(move |tx3: &AppEventSender| {
tx3.review(ReviewTarget::Commit {
sha: sha.clone(),
title: Some(subject.clone()),
});
})],
dismiss_on_select: true,
search_value: Some(search_val),
..Default::default()
});
}
self.bottom_pane.show_selection_view(SelectionViewParams {
title: Some("Select a commit to review".to_string()),
footer_hint: Some(standard_popup_hint_line()),
items,
is_searchable: true,
search_placeholder: Some("Type to search commits".to_string()),
..Default::default()
});
}
pub(crate) fn show_review_custom_prompt(&mut self) {
let tx = self.app_event_tx.clone();
let view = CustomPromptView::new(
"Custom review instructions".to_string(),
"Type instructions and press Enter".to_string(),
/*initial_text*/ String::new(),
/*context_label*/ None,
Box::new(move |prompt: String| {
let trimmed = prompt.trim().to_string();
if trimmed.is_empty() {
return;
}
tx.review(ReviewTarget::Custom {
instructions: trimmed,
});
}),
);
self.bottom_pane.show_view(Box::new(view));
}
}
#[cfg(test)]
pub(crate) fn show_review_commit_picker_with_entries(
chat: &mut ChatWidget,
entries: Vec<CommitLogEntry>,
) {
let mut items: Vec<SelectionItem> = Vec::with_capacity(entries.len());
for entry in entries {
let subject = entry.subject.clone();
let sha = entry.sha.clone();
let search_val = format!("{subject} {sha}");
items.push(SelectionItem {
name: subject.clone(),
actions: vec![Box::new(move |tx3: &AppEventSender| {
tx3.review(ReviewTarget::Commit {
sha: sha.clone(),
title: Some(subject.clone()),
});
})],
dismiss_on_select: true,
search_value: Some(search_val),
..Default::default()
});
}
chat.bottom_pane.show_selection_view(SelectionViewParams {
title: Some("Select a commit to review".to_string()),
footer_hint: Some(standard_popup_hint_line()),
items,
is_searchable: true,
search_placeholder: Some("Type to search commits".to_string()),
..Default::default()
});
}