Files
codex/codex-rs/tui/src/chatwidget/rendering.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

130 lines
4.2 KiB
Rust

//! Render composition for the main chat widget surface.
use super::*;
impl ChatWidget {
pub(super) fn as_renderable(&self) -> RenderableItem<'_> {
let active_cell_right_reserve = self.ambient_pet_wrap_reserved_cols();
let active_cell_renderable = match &self.transcript.active_cell {
Some(cell) => RenderableItem::Owned(Box::new(TranscriptAreaRenderable {
child: cell.as_ref(),
top: 1,
right: active_cell_right_reserve,
})),
None => RenderableItem::Owned(Box::new(())),
};
let active_hook_cell_renderable = match &self.active_hook_cell {
Some(cell) if cell.should_render() => {
RenderableItem::Owned(Box::new(TranscriptAreaRenderable {
child: cell,
top: 1,
right: active_cell_right_reserve,
}))
}
_ => RenderableItem::Owned(Box::new(())),
};
let mut flex = FlexRenderable::new();
flex.push(/*flex*/ 1, active_cell_renderable);
flex.push(/*flex*/ 0, active_hook_cell_renderable);
flex.push(
/*flex*/ 0,
RenderableItem::Owned(Box::new(BottomPaneComposerReserveRenderable {
bottom_pane: &self.bottom_pane,
right_reserve: active_cell_right_reserve,
}))
.inset(Insets::tlbr(
/*top*/ 1, /*left*/ 0, /*bottom*/ 0, /*right*/ 0,
)),
);
RenderableItem::Owned(Box::new(flex))
}
}
struct BottomPaneComposerReserveRenderable<'a> {
bottom_pane: &'a BottomPane,
right_reserve: u16,
}
impl Renderable for BottomPaneComposerReserveRenderable<'_> {
fn render(&self, area: Rect, buf: &mut Buffer) {
self.bottom_pane
.render_with_composer_right_reserve(area, buf, self.right_reserve);
}
fn desired_height(&self, width: u16) -> u16 {
self.bottom_pane
.desired_height_with_composer_right_reserve(width, self.right_reserve)
}
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
self.bottom_pane
.cursor_pos_with_composer_right_reserve(area, self.right_reserve)
}
fn cursor_style(&self, area: Rect) -> crossterm::cursor::SetCursorStyle {
self.bottom_pane
.cursor_style_with_composer_right_reserve(area, self.right_reserve)
}
}
struct TranscriptAreaRenderable<'a> {
child: &'a dyn HistoryCell,
top: u16,
right: u16,
}
impl Renderable for TranscriptAreaRenderable<'_> {
fn render(&self, area: Rect, buf: &mut Buffer) {
let area = self.child_area(area);
let lines = self.child.display_lines(area.width);
let paragraph = Paragraph::new(Text::from(lines)).wrap(Wrap { trim: false });
let y = if area.height == 0 {
0
} else {
let overflow = paragraph
.line_count(area.width)
.saturating_sub(usize::from(area.height));
u16::try_from(overflow).unwrap_or(u16::MAX)
};
Clear.render(area, buf);
paragraph.scroll((y, 0)).render(area, buf);
}
fn desired_height(&self, width: u16) -> u16 {
let child_width = width.saturating_sub(self.right).max(1);
HistoryCell::desired_height(self.child, child_width) + self.top
}
}
impl TranscriptAreaRenderable<'_> {
fn child_area(&self, area: Rect) -> Rect {
let y = area.y.saturating_add(self.top);
let height = area.height.saturating_sub(self.top);
Rect::new(
area.x,
y,
area.width.saturating_sub(self.right).max(1),
height,
)
}
}
impl Renderable for ChatWidget {
fn render(&self, area: Rect, buf: &mut Buffer) {
self.as_renderable().render(area, buf);
self.last_rendered_width.set(Some(area.width as usize));
}
fn desired_height(&self, width: u16) -> u16 {
self.as_renderable().desired_height(width)
}
fn cursor_pos(&self, area: Rect) -> Option<(u16, u16)> {
self.as_renderable().cursor_pos(area)
}
fn cursor_style(&self, area: Rect) -> crossterm::cursor::SetCursorStyle {
self.as_renderable().cursor_style(area)
}
}