refactor onboarding screen to a separate "app" (#2524)

this is in preparation for adding more separate "modes" to the tui, in
particular, a "transcript mode" to view a full history once #2316 lands.

1. split apart "tui events" from "app events".
2. remove onboarding-related events from AppEvent.
3. move several general drawing tools out of App and into a new Tui
class
This commit is contained in:
Jeremy Rose
2025-08-20 13:47:24 -07:00
committed by GitHub
parent 1a1516a80b
commit 0d12380c3b
24 changed files with 755 additions and 803 deletions

View File

@@ -12,13 +12,13 @@ use super::BottomPaneView;
use super::CancellationEvent;
/// Modal overlay asking the user to approve/deny a sequence of requests.
pub(crate) struct ApprovalModalView<'a> {
current: UserApprovalWidget<'a>,
pub(crate) struct ApprovalModalView {
current: UserApprovalWidget,
queue: Vec<ApprovalRequest>,
app_event_tx: AppEventSender,
}
impl ApprovalModalView<'_> {
impl ApprovalModalView {
pub fn new(request: ApprovalRequest, app_event_tx: AppEventSender) -> Self {
Self {
current: UserApprovalWidget::new(request, app_event_tx.clone()),
@@ -41,13 +41,13 @@ impl ApprovalModalView<'_> {
}
}
impl<'a> BottomPaneView<'a> for ApprovalModalView<'a> {
fn handle_key_event(&mut self, _pane: &mut BottomPane<'a>, key_event: KeyEvent) {
impl BottomPaneView for ApprovalModalView {
fn handle_key_event(&mut self, _pane: &mut BottomPane, key_event: KeyEvent) {
self.current.handle_key_event(key_event);
self.maybe_advance();
}
fn on_ctrl_c(&mut self, _pane: &mut BottomPane<'a>) -> CancellationEvent {
fn on_ctrl_c(&mut self, _pane: &mut BottomPane) -> CancellationEvent {
self.current.on_ctrl_c();
self.queue.clear();
CancellationEvent::Handled
@@ -96,6 +96,7 @@ mod tests {
let (tx2, _rx2) = unbounded_channel::<AppEvent>();
let mut pane = BottomPane::new(super::super::BottomPaneParams {
app_event_tx: AppEventSender::new(tx2),
frame_requester: crate::tui::FrameRequester::test_dummy(),
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),

View File

@@ -7,10 +7,10 @@ use super::BottomPane;
use super::CancellationEvent;
/// Trait implemented by every view that can be shown in the bottom pane.
pub(crate) trait BottomPaneView<'a> {
pub(crate) trait BottomPaneView {
/// Handle a key event while the view is active. A redraw is always
/// scheduled after this call.
fn handle_key_event(&mut self, _pane: &mut BottomPane<'a>, _key_event: KeyEvent) {}
fn handle_key_event(&mut self, _pane: &mut BottomPane, _key_event: KeyEvent) {}
/// Return `true` if the view has finished and should be removed.
fn is_complete(&self) -> bool {
@@ -18,7 +18,7 @@ pub(crate) trait BottomPaneView<'a> {
}
/// Handle Ctrl-C while this view is active.
fn on_ctrl_c(&mut self, _pane: &mut BottomPane<'a>) -> CancellationEvent {
fn on_ctrl_c(&mut self, _pane: &mut BottomPane) -> CancellationEvent {
CancellationEvent::Ignored
}

View File

@@ -105,8 +105,8 @@ impl ListSelectionView {
}
}
impl BottomPaneView<'_> for ListSelectionView {
fn handle_key_event(&mut self, _pane: &mut BottomPane<'_>, key_event: KeyEvent) {
impl BottomPaneView for ListSelectionView {
fn handle_key_event(&mut self, _pane: &mut BottomPane, key_event: KeyEvent) {
match key_event {
KeyEvent {
code: KeyCode::Up, ..
@@ -131,7 +131,7 @@ impl BottomPaneView<'_> for ListSelectionView {
self.complete
}
fn on_ctrl_c(&mut self, _pane: &mut BottomPane<'_>) -> CancellationEvent {
fn on_ctrl_c(&mut self, _pane: &mut BottomPane) -> CancellationEvent {
self.complete = true;
CancellationEvent::Handled
}

View File

@@ -1,7 +1,7 @@
//! Bottom pane: shows the ChatComposer or a BottomPaneView, if one is active.
use crate::app_event::AppEvent;
use crate::app_event_sender::AppEventSender;
use crate::tui::FrameRequester;
use crate::user_approval_widget::ApprovalRequest;
use bottom_pane_view::BottomPaneView;
use codex_core::protocol::TokenUsage;
@@ -39,15 +39,17 @@ pub(crate) use list_selection_view::SelectionItem;
use status_indicator_view::StatusIndicatorView;
/// Pane displayed in the lower half of the chat UI.
pub(crate) struct BottomPane<'a> {
pub(crate) struct BottomPane {
/// Composer is retained even when a BottomPaneView is displayed so the
/// input state is retained when the view is closed.
composer: ChatComposer,
/// If present, this is displayed instead of the `composer`.
active_view: Option<Box<dyn BottomPaneView<'a> + 'a>>,
active_view: Option<Box<dyn BottomPaneView>>,
app_event_tx: AppEventSender,
frame_requester: FrameRequester,
has_input_focus: bool,
is_task_running: bool,
ctrl_c_quit_hint: bool,
@@ -59,12 +61,13 @@ pub(crate) struct BottomPane<'a> {
pub(crate) struct BottomPaneParams {
pub(crate) app_event_tx: AppEventSender,
pub(crate) frame_requester: FrameRequester,
pub(crate) has_input_focus: bool,
pub(crate) enhanced_keys_supported: bool,
pub(crate) placeholder_text: String,
}
impl BottomPane<'_> {
impl BottomPane {
const BOTTOM_PAD_LINES: u16 = 2;
pub fn new(params: BottomPaneParams) -> Self {
let enhanced_keys_supported = params.enhanced_keys_supported;
@@ -77,6 +80,7 @@ impl BottomPane<'_> {
),
active_view: None,
app_event_tx: params.app_event_tx,
frame_requester: params.frame_requester,
has_input_focus: params.has_input_focus,
is_task_running: false,
ctrl_c_quit_hint: false,
@@ -113,7 +117,10 @@ impl BottomPane<'_> {
if !view.is_complete() {
self.active_view = Some(view);
} else if self.is_task_running {
let mut v = StatusIndicatorView::new(self.app_event_tx.clone());
let mut v = StatusIndicatorView::new(
self.app_event_tx.clone(),
self.frame_requester.clone(),
);
v.update_text("waiting for model".to_string());
self.active_view = Some(Box::new(v));
self.status_view_active = true;
@@ -144,7 +151,10 @@ impl BottomPane<'_> {
self.active_view = Some(view);
} else if self.is_task_running {
// Modal aborted but task still running restore status indicator.
let mut v = StatusIndicatorView::new(self.app_event_tx.clone());
let mut v = StatusIndicatorView::new(
self.app_event_tx.clone(),
self.frame_requester.clone(),
);
v.update_text("waiting for model".to_string());
self.active_view = Some(Box::new(v));
self.status_view_active = true;
@@ -199,6 +209,7 @@ impl BottomPane<'_> {
if self.active_view.is_none() {
self.active_view = Some(Box::new(StatusIndicatorView::new(
self.app_event_tx.clone(),
self.frame_requester.clone(),
)));
self.status_view_active = true;
}
@@ -292,7 +303,7 @@ impl BottomPane<'_> {
/// Height (terminal rows) required by the current bottom pane.
pub(crate) fn request_redraw(&self) {
self.app_event_tx.send(AppEvent::RequestRedraw)
self.frame_requester.schedule_frame();
}
// --- History helpers ---
@@ -322,7 +333,7 @@ impl BottomPane<'_> {
}
}
impl WidgetRef for &BottomPane<'_> {
impl WidgetRef for &BottomPane {
fn render_ref(&self, area: Rect, buf: &mut Buffer) {
if let Some(view) = &self.active_view {
// Reserve bottom padding lines; keep at least 1 line for the view.
@@ -375,6 +386,7 @@ mod tests {
let tx = AppEventSender::new(tx_raw);
let mut pane = BottomPane::new(BottomPaneParams {
app_event_tx: tx,
frame_requester: crate::tui::FrameRequester::test_dummy(),
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
@@ -393,6 +405,7 @@ mod tests {
let tx = AppEventSender::new(tx_raw);
let mut pane = BottomPane::new(BottomPaneParams {
app_event_tx: tx,
frame_requester: crate::tui::FrameRequester::test_dummy(),
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
@@ -422,6 +435,7 @@ mod tests {
let tx = AppEventSender::new(tx_raw);
let mut pane = BottomPane::new(BottomPaneParams {
app_event_tx: tx.clone(),
frame_requester: crate::tui::FrameRequester::test_dummy(),
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
@@ -472,6 +486,7 @@ mod tests {
let tx = AppEventSender::new(tx_raw);
let mut pane = BottomPane::new(BottomPaneParams {
app_event_tx: tx,
frame_requester: crate::tui::FrameRequester::test_dummy(),
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
@@ -504,6 +519,7 @@ mod tests {
let tx = AppEventSender::new(tx_raw);
let mut pane = BottomPane::new(BottomPaneParams {
app_event_tx: tx,
frame_requester: crate::tui::FrameRequester::test_dummy(),
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),
@@ -556,6 +572,7 @@ mod tests {
let tx = AppEventSender::new(tx_raw);
let mut pane = BottomPane::new(BottomPaneParams {
app_event_tx: tx,
frame_requester: crate::tui::FrameRequester::test_dummy(),
has_input_focus: true,
enhanced_keys_supported: false,
placeholder_text: "Ask Codex to do anything".to_string(),

View File

@@ -6,6 +6,7 @@ use ratatui::widgets::WidgetRef;
use crate::app_event_sender::AppEventSender;
use crate::bottom_pane::BottomPane;
use crate::status_indicator_widget::StatusIndicatorWidget;
use crate::tui::FrameRequester;
use super::BottomPaneView;
@@ -14,9 +15,9 @@ pub(crate) struct StatusIndicatorView {
}
impl StatusIndicatorView {
pub fn new(app_event_tx: AppEventSender) -> Self {
pub fn new(app_event_tx: AppEventSender, frame_requester: FrameRequester) -> Self {
Self {
view: StatusIndicatorWidget::new(app_event_tx),
view: StatusIndicatorWidget::new(app_event_tx, frame_requester),
}
}
@@ -25,7 +26,7 @@ impl StatusIndicatorView {
}
}
impl BottomPaneView<'_> for StatusIndicatorView {
impl BottomPaneView for StatusIndicatorView {
fn should_hide_when_task_is_done(&mut self) -> bool {
true
}
@@ -38,7 +39,7 @@ impl BottomPaneView<'_> for StatusIndicatorView {
self.view.render_ref(area, buf);
}
fn handle_key_event(&mut self, _pane: &mut BottomPane<'_>, key_event: KeyEvent) {
fn handle_key_event(&mut self, _pane: &mut BottomPane, key_event: KeyEvent) {
if key_event.code == KeyCode::Esc {
self.view.interrupt();
}