feat: introduce CellWidget trait (#1148)
The motivation behind this PR is to make it so a `HistoryCell` is more like a `WidgetRef` that knows how to render itself into a `Rect` so that it can be backed by something other than a `Vec<Line>`. Because a `HistoryCell` is intended to appear in a scrollable list, we want to ensure the stack of cells can be scrolled one `Line` at a time even if the `HistoryCell` is not backed by a `Vec<Line>` itself. To this end, we introduce the `CellWidget` trait whose key method is: ``` fn render_window(&self, first_visible_line: usize, area: Rect, buf: &mut Buffer); ``` The `first_visible_line` param is what differs from `WidgetRef::render_ref()`, as a `CellWidget` needs to know the offset into its "full view" at which it should start rendering. The bookkeeping in `ConversationHistoryWidget` has been updated accordingly to ensure each `CellWidget` in the history is rendered appropriately.
This commit is contained in:
20
codex-rs/tui/src/cell_widget.rs
Normal file
20
codex-rs/tui/src/cell_widget.rs
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
use ratatui::prelude::*;
|
||||||
|
|
||||||
|
/// Trait implemented by every type that can live inside the conversation
|
||||||
|
/// history list. It provides two primitives that the parent scroll-view
|
||||||
|
/// needs: how *tall* the widget is at a given width and how to render an
|
||||||
|
/// arbitrary contiguous *window* of that widget.
|
||||||
|
///
|
||||||
|
/// The `first_visible_line` argument to [`render_window`] allows partial
|
||||||
|
/// rendering when the top of the widget is scrolled off-screen. The caller
|
||||||
|
/// guarantees that `first_visible_line + area.height as usize` never exceeds
|
||||||
|
/// the total height previously returned by [`height`].
|
||||||
|
pub(crate) trait CellWidget {
|
||||||
|
/// Total height measured in wrapped terminal lines when drawn with the
|
||||||
|
/// given *content* width (no scrollbar column included).
|
||||||
|
fn height(&self, width: u16) -> usize;
|
||||||
|
|
||||||
|
/// Render a *window* that starts `first_visible_line` lines below the top
|
||||||
|
/// of the widget. The window’s size is given by `area`.
|
||||||
|
fn render_window(&self, first_visible_line: usize, area: Rect, buf: &mut Buffer);
|
||||||
|
}
|
||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use crate::cell_widget::CellWidget;
|
||||||
use crate::history_cell::CommandOutput;
|
use crate::history_cell::CommandOutput;
|
||||||
use crate::history_cell::HistoryCell;
|
use crate::history_cell::HistoryCell;
|
||||||
use crate::history_cell::PatchEventType;
|
use crate::history_cell::PatchEventType;
|
||||||
@@ -236,11 +237,7 @@ impl ConversationHistoryWidget {
|
|||||||
|
|
||||||
fn add_to_history(&mut self, cell: HistoryCell) {
|
fn add_to_history(&mut self, cell: HistoryCell) {
|
||||||
let width = self.cached_width.get();
|
let width = self.cached_width.get();
|
||||||
let count = if width > 0 {
|
let count = if width > 0 { cell.height(width) } else { 0 };
|
||||||
wrapped_line_count_for_cell(&cell, width)
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
self.entries.push(Entry {
|
self.entries.push(Entry {
|
||||||
cell,
|
cell,
|
||||||
@@ -284,9 +281,7 @@ impl ConversationHistoryWidget {
|
|||||||
|
|
||||||
// Update cached line count.
|
// Update cached line count.
|
||||||
if width > 0 {
|
if width > 0 {
|
||||||
entry
|
entry.line_count.set(cell.height(width));
|
||||||
.line_count
|
|
||||||
.set(wrapped_line_count_for_cell(cell, width));
|
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -328,9 +323,7 @@ impl ConversationHistoryWidget {
|
|||||||
entry.cell = completed;
|
entry.cell = completed;
|
||||||
|
|
||||||
if width > 0 {
|
if width > 0 {
|
||||||
entry
|
entry.line_count.set(entry.cell.height(width));
|
||||||
.line_count
|
|
||||||
.set(wrapped_line_count_for_cell(&entry.cell, width));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
@@ -378,7 +371,7 @@ impl WidgetRef for ConversationHistoryWidget {
|
|||||||
|
|
||||||
let mut num_lines: usize = 0;
|
let mut num_lines: usize = 0;
|
||||||
for entry in &self.entries {
|
for entry in &self.entries {
|
||||||
let count = wrapped_line_count_for_cell(&entry.cell, effective_width);
|
let count = entry.cell.height(effective_width);
|
||||||
num_lines += count;
|
num_lines += count;
|
||||||
entry.line_count.set(count);
|
entry.line_count.set(count);
|
||||||
}
|
}
|
||||||
@@ -397,79 +390,69 @@ impl WidgetRef for ConversationHistoryWidget {
|
|||||||
self.scroll_position.min(max_scroll)
|
self.scroll_position.min(max_scroll)
|
||||||
};
|
};
|
||||||
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
// Build a *window* into the history so we only clone the `Line`s that
|
|
||||||
// may actually be visible in this frame. We still hand the slice off
|
|
||||||
// to a `Paragraph` with an additional scroll offset to avoid slicing
|
|
||||||
// inside a wrapped line (we don’t have per-subline granularity).
|
|
||||||
// ------------------------------------------------------------------
|
|
||||||
|
|
||||||
// Find the first entry that intersects the current scroll position.
|
|
||||||
let mut cumulative = 0usize;
|
|
||||||
let mut first_idx = 0usize;
|
|
||||||
for (idx, entry) in self.entries.iter().enumerate() {
|
|
||||||
let next = cumulative + entry.line_count.get();
|
|
||||||
if next > scroll_pos {
|
|
||||||
first_idx = idx;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
cumulative = next;
|
|
||||||
}
|
|
||||||
|
|
||||||
let offset_into_first = scroll_pos - cumulative;
|
|
||||||
|
|
||||||
// Collect enough raw lines from `first_idx` onward to cover the
|
|
||||||
// viewport. We may fetch *slightly* more than necessary (whole cells)
|
|
||||||
// but never the entire history.
|
|
||||||
let mut collected_wrapped = 0usize;
|
|
||||||
let mut visible_lines: Vec<Line<'static>> = Vec::new();
|
|
||||||
|
|
||||||
for entry in &self.entries[first_idx..] {
|
|
||||||
visible_lines.extend(entry.cell.lines().iter().cloned());
|
|
||||||
collected_wrapped += entry.line_count.get();
|
|
||||||
if collected_wrapped >= offset_into_first + viewport_height {
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build the Paragraph with wrapping enabled so long lines are not
|
|
||||||
// clipped. Apply vertical scroll so that `offset_into_first` wrapped
|
|
||||||
// lines are hidden at the top.
|
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
// Render order:
|
// Render order:
|
||||||
// 1. Clear the whole widget area so we do not leave behind any glyphs
|
// 1. Clear full widget area (avoid artifacts from prior frame).
|
||||||
// from the previous frame.
|
|
||||||
// 2. Draw the surrounding Block (border and title).
|
// 2. Draw the surrounding Block (border and title).
|
||||||
// 3. Draw the Paragraph inside the Block, **leaving the right-most
|
// 3. Render *each* visible HistoryCell into its own sub-Rect while
|
||||||
// column free** for the scrollbar.
|
// respecting partial visibility at the top and bottom.
|
||||||
// 4. Finally draw the scrollbar (if needed).
|
// 4. Draw the scrollbar track / thumb in the reserved column.
|
||||||
// ------------------------------------------------------------------
|
// ------------------------------------------------------------------
|
||||||
|
|
||||||
// Clear the widget area to avoid visual artifacts from previous frames.
|
// Clear entire widget area first.
|
||||||
Clear.render(area, buf);
|
Clear.render(area, buf);
|
||||||
|
|
||||||
// Draw the outer border and title first so the Paragraph does not
|
// Draw border + title.
|
||||||
// overwrite it.
|
|
||||||
block.render(area, buf);
|
block.render(area, buf);
|
||||||
|
|
||||||
// Area available for text after accounting for the scrollbar.
|
// ------------------------------------------------------------------
|
||||||
let text_area = Rect {
|
// Calculate which cells are visible for the current scroll position
|
||||||
x: inner.x,
|
// and paint them one by one.
|
||||||
y: inner.y,
|
// ------------------------------------------------------------------
|
||||||
width: effective_width,
|
|
||||||
height: inner.height,
|
|
||||||
};
|
|
||||||
|
|
||||||
let paragraph = Paragraph::new(visible_lines)
|
let mut y_cursor = inner.y; // first line inside viewport
|
||||||
.wrap(wrap_cfg())
|
let mut remaining_height = inner.height as usize;
|
||||||
.scroll((offset_into_first as u16, 0));
|
let mut lines_to_skip = scroll_pos; // number of wrapped lines to skip (above viewport)
|
||||||
|
|
||||||
paragraph.render(text_area, buf);
|
for entry in &self.entries {
|
||||||
|
let cell_height = entry.line_count.get();
|
||||||
|
|
||||||
// Always render a scrollbar *track* so that the reserved column is
|
// Completely above viewport? Skip whole cell.
|
||||||
// visually filled, even when the content fits within the viewport.
|
if lines_to_skip >= cell_height {
|
||||||
// We only draw the *thumb* when the content actually overflows.
|
lines_to_skip -= cell_height;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine how much of this cell is visible.
|
||||||
|
let visible_height = (cell_height - lines_to_skip).min(remaining_height);
|
||||||
|
|
||||||
|
if visible_height == 0 {
|
||||||
|
break; // no space left
|
||||||
|
}
|
||||||
|
|
||||||
|
let cell_rect = Rect {
|
||||||
|
x: inner.x,
|
||||||
|
y: y_cursor,
|
||||||
|
width: effective_width,
|
||||||
|
height: visible_height as u16,
|
||||||
|
};
|
||||||
|
|
||||||
|
entry.cell.render_window(lines_to_skip, cell_rect, buf);
|
||||||
|
|
||||||
|
// Advance cursor inside viewport.
|
||||||
|
y_cursor += visible_height as u16;
|
||||||
|
remaining_height -= visible_height;
|
||||||
|
|
||||||
|
// After the first (possibly partially skipped) cell, we no longer
|
||||||
|
// need to skip lines at the top.
|
||||||
|
lines_to_skip = 0;
|
||||||
|
|
||||||
|
if remaining_height == 0 {
|
||||||
|
break; // viewport filled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Always render a scrollbar *track* so the reserved column is filled.
|
||||||
let overflow = num_lines.saturating_sub(viewport_height);
|
let overflow = num_lines.saturating_sub(viewport_height);
|
||||||
|
|
||||||
let mut scroll_state = ScrollbarState::default()
|
let mut scroll_state = ScrollbarState::default()
|
||||||
@@ -521,15 +504,6 @@ impl WidgetRef for ConversationHistoryWidget {
|
|||||||
/// Common [`Wrap`] configuration used for both measurement and rendering so
|
/// Common [`Wrap`] configuration used for both measurement and rendering so
|
||||||
/// they stay in sync.
|
/// they stay in sync.
|
||||||
#[inline]
|
#[inline]
|
||||||
const fn wrap_cfg() -> ratatui::widgets::Wrap {
|
pub(crate) const fn wrap_cfg() -> ratatui::widgets::Wrap {
|
||||||
ratatui::widgets::Wrap { trim: false }
|
ratatui::widgets::Wrap { trim: false }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the wrapped line count for `cell` at the given `width` using the
|
|
||||||
/// same wrapping rules that `ConversationHistoryWidget` uses during
|
|
||||||
/// rendering.
|
|
||||||
fn wrapped_line_count_for_cell(cell: &HistoryCell, width: u16) -> usize {
|
|
||||||
Paragraph::new(cell.lines().clone())
|
|
||||||
.wrap(wrap_cfg())
|
|
||||||
.line_count(width)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ use ratatui::style::Modifier;
|
|||||||
use ratatui::style::Style;
|
use ratatui::style::Style;
|
||||||
use ratatui::text::Line as RtLine;
|
use ratatui::text::Line as RtLine;
|
||||||
use ratatui::text::Span as RtSpan;
|
use ratatui::text::Span as RtSpan;
|
||||||
|
|
||||||
|
use crate::cell_widget::CellWidget;
|
||||||
|
use crate::text_block::TextBlock;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
@@ -34,16 +37,16 @@ pub(crate) enum PatchEventType {
|
|||||||
/// scrollable list.
|
/// scrollable list.
|
||||||
pub(crate) enum HistoryCell {
|
pub(crate) enum HistoryCell {
|
||||||
/// Welcome message.
|
/// Welcome message.
|
||||||
WelcomeMessage { lines: Vec<Line<'static>> },
|
WelcomeMessage { view: TextBlock },
|
||||||
|
|
||||||
/// Message from the user.
|
/// Message from the user.
|
||||||
UserPrompt { lines: Vec<Line<'static>> },
|
UserPrompt { view: TextBlock },
|
||||||
|
|
||||||
/// Message from the agent.
|
/// Message from the agent.
|
||||||
AgentMessage { lines: Vec<Line<'static>> },
|
AgentMessage { view: TextBlock },
|
||||||
|
|
||||||
/// Reasoning event from the agent.
|
/// Reasoning event from the agent.
|
||||||
AgentReasoning { lines: Vec<Line<'static>> },
|
AgentReasoning { view: TextBlock },
|
||||||
|
|
||||||
/// An exec tool call that has not finished yet.
|
/// An exec tool call that has not finished yet.
|
||||||
ActiveExecCommand {
|
ActiveExecCommand {
|
||||||
@@ -51,11 +54,11 @@ pub(crate) enum HistoryCell {
|
|||||||
/// The shell command, escaped and formatted.
|
/// The shell command, escaped and formatted.
|
||||||
command: String,
|
command: String,
|
||||||
start: Instant,
|
start: Instant,
|
||||||
lines: Vec<Line<'static>>,
|
view: TextBlock,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Completed exec tool call.
|
/// Completed exec tool call.
|
||||||
CompletedExecCommand { lines: Vec<Line<'static>> },
|
CompletedExecCommand { view: TextBlock },
|
||||||
|
|
||||||
/// An MCP tool call that has not finished yet.
|
/// An MCP tool call that has not finished yet.
|
||||||
ActiveMcpToolCall {
|
ActiveMcpToolCall {
|
||||||
@@ -67,29 +70,25 @@ pub(crate) enum HistoryCell {
|
|||||||
/// exact same text without re-formatting.
|
/// exact same text without re-formatting.
|
||||||
invocation: String,
|
invocation: String,
|
||||||
start: Instant,
|
start: Instant,
|
||||||
lines: Vec<Line<'static>>,
|
view: TextBlock,
|
||||||
},
|
},
|
||||||
|
|
||||||
/// Completed MCP tool call.
|
/// Completed MCP tool call.
|
||||||
CompletedMcpToolCall { lines: Vec<Line<'static>> },
|
CompletedMcpToolCall { view: TextBlock },
|
||||||
|
|
||||||
/// Background event
|
/// Background event.
|
||||||
BackgroundEvent { lines: Vec<Line<'static>> },
|
BackgroundEvent { view: TextBlock },
|
||||||
|
|
||||||
/// Error event from the backend.
|
/// Error event from the backend.
|
||||||
ErrorEvent { lines: Vec<Line<'static>> },
|
ErrorEvent { view: TextBlock },
|
||||||
|
|
||||||
/// Info describing the newly‑initialized session.
|
/// Info describing the newly-initialized session.
|
||||||
SessionInfo { lines: Vec<Line<'static>> },
|
SessionInfo { view: TextBlock },
|
||||||
|
|
||||||
/// A pending code patch that is awaiting user approval. Mirrors the
|
/// A pending code patch that is awaiting user approval. Mirrors the
|
||||||
/// behaviour of `ActiveExecCommand` so the user sees *what* patch the
|
/// behaviour of `ActiveExecCommand` so the user sees *what* patch the
|
||||||
/// model wants to apply before being prompted to approve or deny it.
|
/// model wants to apply before being prompted to approve or deny it.
|
||||||
PendingPatch {
|
PendingPatch { view: TextBlock },
|
||||||
/// Identifier so that a future `PatchApplyEnd` can update the entry
|
|
||||||
/// with the final status (not yet implemented).
|
|
||||||
lines: Vec<Line<'static>>,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const TOOL_CALL_MAX_LINES: usize = 5;
|
const TOOL_CALL_MAX_LINES: usize = 5;
|
||||||
@@ -132,9 +131,13 @@ impl HistoryCell {
|
|||||||
lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()]));
|
lines.push(Line::from(vec![format!("{key}: ").bold(), value.into()]));
|
||||||
}
|
}
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
HistoryCell::WelcomeMessage { lines }
|
HistoryCell::WelcomeMessage {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
}
|
||||||
} else if config.model == model {
|
} else if config.model == model {
|
||||||
HistoryCell::SessionInfo { lines: vec![] }
|
HistoryCell::SessionInfo {
|
||||||
|
view: TextBlock::new(Vec::new()),
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
let lines = vec![
|
let lines = vec![
|
||||||
Line::from("model changed:".magenta().bold()),
|
Line::from("model changed:".magenta().bold()),
|
||||||
@@ -142,7 +145,9 @@ impl HistoryCell {
|
|||||||
Line::from(format!("used: {}", model)),
|
Line::from(format!("used: {}", model)),
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
];
|
];
|
||||||
HistoryCell::SessionInfo { lines }
|
HistoryCell::SessionInfo {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +157,9 @@ impl HistoryCell {
|
|||||||
lines.extend(message.lines().map(|l| Line::from(l.to_string())));
|
lines.extend(message.lines().map(|l| Line::from(l.to_string())));
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
|
|
||||||
HistoryCell::UserPrompt { lines }
|
HistoryCell::UserPrompt {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_agent_message(config: &Config, message: String) -> Self {
|
pub(crate) fn new_agent_message(config: &Config, message: String) -> Self {
|
||||||
@@ -161,7 +168,9 @@ impl HistoryCell {
|
|||||||
append_markdown(&message, &mut lines, config);
|
append_markdown(&message, &mut lines, config);
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
|
|
||||||
HistoryCell::AgentMessage { lines }
|
HistoryCell::AgentMessage {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_agent_reasoning(config: &Config, text: String) -> Self {
|
pub(crate) fn new_agent_reasoning(config: &Config, text: String) -> Self {
|
||||||
@@ -170,7 +179,9 @@ impl HistoryCell {
|
|||||||
append_markdown(&text, &mut lines, config);
|
append_markdown(&text, &mut lines, config);
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
|
|
||||||
HistoryCell::AgentReasoning { lines }
|
HistoryCell::AgentReasoning {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_active_exec_command(call_id: String, command: Vec<String>) -> Self {
|
pub(crate) fn new_active_exec_command(call_id: String, command: Vec<String>) -> Self {
|
||||||
@@ -187,7 +198,7 @@ impl HistoryCell {
|
|||||||
call_id,
|
call_id,
|
||||||
command: command_escaped,
|
command: command_escaped,
|
||||||
start,
|
start,
|
||||||
lines,
|
view: TextBlock::new(lines),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -226,7 +237,9 @@ impl HistoryCell {
|
|||||||
}
|
}
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
|
|
||||||
HistoryCell::CompletedExecCommand { lines }
|
HistoryCell::CompletedExecCommand {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_active_mcp_tool_call(
|
pub(crate) fn new_active_mcp_tool_call(
|
||||||
@@ -267,7 +280,7 @@ impl HistoryCell {
|
|||||||
fq_tool_name,
|
fq_tool_name,
|
||||||
invocation,
|
invocation,
|
||||||
start,
|
start,
|
||||||
lines,
|
view: TextBlock::new(lines),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,7 +317,9 @@ impl HistoryCell {
|
|||||||
|
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
|
|
||||||
HistoryCell::CompletedMcpToolCall { lines }
|
HistoryCell::CompletedMcpToolCall {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_background_event(message: String) -> Self {
|
pub(crate) fn new_background_event(message: String) -> Self {
|
||||||
@@ -312,7 +327,9 @@ impl HistoryCell {
|
|||||||
lines.push(Line::from("event".dim()));
|
lines.push(Line::from("event".dim()));
|
||||||
lines.extend(message.lines().map(|l| Line::from(l.to_string()).dim()));
|
lines.extend(message.lines().map(|l| Line::from(l.to_string()).dim()));
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
HistoryCell::BackgroundEvent { lines }
|
HistoryCell::BackgroundEvent {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new_error_event(message: String) -> Self {
|
pub(crate) fn new_error_event(message: String) -> Self {
|
||||||
@@ -320,7 +337,9 @@ impl HistoryCell {
|
|||||||
vec!["ERROR: ".red().bold(), message.into()].into(),
|
vec!["ERROR: ".red().bold(), message.into()].into(),
|
||||||
"".into(),
|
"".into(),
|
||||||
];
|
];
|
||||||
HistoryCell::ErrorEvent { lines }
|
HistoryCell::ErrorEvent {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a new `PendingPatch` cell that lists the file‑level summary of
|
/// Create a new `PendingPatch` cell that lists the file‑level summary of
|
||||||
@@ -339,7 +358,9 @@ impl HistoryCell {
|
|||||||
auto_approved: false,
|
auto_approved: false,
|
||||||
} => {
|
} => {
|
||||||
let lines = vec![Line::from("patch applied".magenta().bold())];
|
let lines = vec![Line::from("patch applied".magenta().bold())];
|
||||||
return Self::PendingPatch { lines };
|
return Self::PendingPatch {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -380,23 +401,52 @@ impl HistoryCell {
|
|||||||
|
|
||||||
lines.push(Line::from(""));
|
lines.push(Line::from(""));
|
||||||
|
|
||||||
HistoryCell::PendingPatch { lines }
|
HistoryCell::PendingPatch {
|
||||||
|
view: TextBlock::new(lines),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// `CellWidget` implementation – most variants delegate to their internal
|
||||||
|
// `TextBlock`. Variants that need custom painting can add their own logic in
|
||||||
|
// the match arms.
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
impl CellWidget for HistoryCell {
|
||||||
|
fn height(&self, width: u16) -> usize {
|
||||||
|
match self {
|
||||||
|
HistoryCell::WelcomeMessage { view }
|
||||||
|
| HistoryCell::UserPrompt { view }
|
||||||
|
| HistoryCell::AgentMessage { view }
|
||||||
|
| HistoryCell::AgentReasoning { view }
|
||||||
|
| HistoryCell::BackgroundEvent { view }
|
||||||
|
| HistoryCell::ErrorEvent { view }
|
||||||
|
| HistoryCell::SessionInfo { view }
|
||||||
|
| HistoryCell::CompletedExecCommand { view }
|
||||||
|
| HistoryCell::CompletedMcpToolCall { view }
|
||||||
|
| HistoryCell::PendingPatch { view }
|
||||||
|
| HistoryCell::ActiveExecCommand { view, .. }
|
||||||
|
| HistoryCell::ActiveMcpToolCall { view, .. } => view.height(width),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn lines(&self) -> &Vec<Line<'static>> {
|
fn render_window(&self, first_visible_line: usize, area: Rect, buf: &mut Buffer) {
|
||||||
match self {
|
match self {
|
||||||
HistoryCell::WelcomeMessage { lines, .. }
|
HistoryCell::WelcomeMessage { view }
|
||||||
| HistoryCell::UserPrompt { lines, .. }
|
| HistoryCell::UserPrompt { view }
|
||||||
| HistoryCell::AgentMessage { lines, .. }
|
| HistoryCell::AgentMessage { view }
|
||||||
| HistoryCell::AgentReasoning { lines, .. }
|
| HistoryCell::AgentReasoning { view }
|
||||||
| HistoryCell::BackgroundEvent { lines, .. }
|
| HistoryCell::BackgroundEvent { view }
|
||||||
| HistoryCell::ErrorEvent { lines, .. }
|
| HistoryCell::ErrorEvent { view }
|
||||||
| HistoryCell::SessionInfo { lines, .. }
|
| HistoryCell::SessionInfo { view }
|
||||||
| HistoryCell::ActiveExecCommand { lines, .. }
|
| HistoryCell::CompletedExecCommand { view }
|
||||||
| HistoryCell::CompletedExecCommand { lines, .. }
|
| HistoryCell::CompletedMcpToolCall { view }
|
||||||
| HistoryCell::ActiveMcpToolCall { lines, .. }
|
| HistoryCell::PendingPatch { view }
|
||||||
| HistoryCell::CompletedMcpToolCall { lines, .. }
|
| HistoryCell::ActiveExecCommand { view, .. }
|
||||||
| HistoryCell::PendingPatch { lines, .. } => lines,
|
| HistoryCell::ActiveMcpToolCall { view, .. } => {
|
||||||
|
view.render_window(first_visible_line, area, buf)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ mod app;
|
|||||||
mod app_event;
|
mod app_event;
|
||||||
mod app_event_sender;
|
mod app_event_sender;
|
||||||
mod bottom_pane;
|
mod bottom_pane;
|
||||||
|
mod cell_widget;
|
||||||
mod chatwidget;
|
mod chatwidget;
|
||||||
mod citation_regex;
|
mod citation_regex;
|
||||||
mod cli;
|
mod cli;
|
||||||
@@ -32,6 +33,7 @@ mod mouse_capture;
|
|||||||
mod scroll_event_helper;
|
mod scroll_event_helper;
|
||||||
mod slash_command;
|
mod slash_command;
|
||||||
mod status_indicator_widget;
|
mod status_indicator_widget;
|
||||||
|
mod text_block;
|
||||||
mod tui;
|
mod tui;
|
||||||
mod user_approval_widget;
|
mod user_approval_widget;
|
||||||
|
|
||||||
|
|||||||
32
codex-rs/tui/src/text_block.rs
Normal file
32
codex-rs/tui/src/text_block.rs
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
use crate::cell_widget::CellWidget;
|
||||||
|
use ratatui::prelude::*;
|
||||||
|
|
||||||
|
/// A simple widget that just displays a list of `Line`s via a `Paragraph`.
|
||||||
|
/// This is the default rendering backend for most `HistoryCell` variants.
|
||||||
|
#[derive(Clone)]
|
||||||
|
pub(crate) struct TextBlock {
|
||||||
|
pub(crate) lines: Vec<Line<'static>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl TextBlock {
|
||||||
|
pub(crate) fn new(lines: Vec<Line<'static>>) -> Self {
|
||||||
|
Self { lines }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl CellWidget for TextBlock {
|
||||||
|
fn height(&self, width: u16) -> usize {
|
||||||
|
// Use the same wrapping configuration as ConversationHistoryWidget so
|
||||||
|
// measurement stays in sync with rendering.
|
||||||
|
ratatui::widgets::Paragraph::new(self.lines.clone())
|
||||||
|
.wrap(crate::conversation_history_widget::wrap_cfg())
|
||||||
|
.line_count(width)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_window(&self, first_visible_line: usize, area: Rect, buf: &mut Buffer) {
|
||||||
|
ratatui::widgets::Paragraph::new(self.lines.clone())
|
||||||
|
.wrap(crate::conversation_history_widget::wrap_cfg())
|
||||||
|
.scroll((first_visible_line as u16, 0))
|
||||||
|
.render(area, buf);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user