mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
3c3e18c222
## 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.
130 lines
4.2 KiB
Rust
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)
|
|
}
|
|
}
|