Stream model responses (#1810)

Stream models thoughts and responses instead of waiting for the whole
thing to come through. Very rough right now, but I'm making the risk call to push through.
This commit is contained in:
easong-openai
2025-08-04 21:23:22 -07:00
committed by GitHub
parent 063083af15
commit 906d449760
17 changed files with 1616 additions and 234 deletions

View File

@@ -0,0 +1,214 @@
#![cfg(feature = "vt100-tests")]
#![expect(clippy::expect_used)]
use ratatui::backend::TestBackend;
use ratatui::layout::Rect;
use ratatui::style::Color;
use ratatui::style::Style;
use ratatui::text::Line;
use ratatui::text::Span;
// Small helper macro to assert a collection contains an item with a clearer
// failure message.
macro_rules! assert_contains {
($collection:expr, $item:expr $(,)?) => {
assert!(
$collection.contains(&$item),
"Expected {:?} to contain {:?}",
$collection,
$item
);
};
($collection:expr, $item:expr, $($arg:tt)+) => {
assert!($collection.contains(&$item), $($arg)+);
};
}
struct TestScenario {
width: u16,
height: u16,
term: codex_tui::custom_terminal::Terminal<TestBackend>,
}
impl TestScenario {
fn new(width: u16, height: u16, viewport: Rect) -> Self {
let backend = TestBackend::new(width, height);
let mut term = codex_tui::custom_terminal::Terminal::with_options(backend)
.expect("failed to construct terminal");
term.set_viewport_area(viewport);
Self {
width,
height,
term,
}
}
fn run_insert(&mut self, lines: Vec<Line<'static>>) -> Vec<u8> {
let mut buf: Vec<u8> = Vec::new();
codex_tui::insert_history::insert_history_lines_to_writer(&mut self.term, &mut buf, lines);
buf
}
fn screen_rows_from_bytes(&self, bytes: &[u8]) -> Vec<String> {
let mut parser = vt100::Parser::new(self.height, self.width, 0);
parser.process(bytes);
let screen = parser.screen();
let mut rows: Vec<String> = Vec::with_capacity(self.height as usize);
for row in 0..self.height {
let mut s = String::with_capacity(self.width as usize);
for col in 0..self.width {
if let Some(cell) = screen.cell(row, col) {
if let Some(ch) = cell.contents().chars().next() {
s.push(ch);
} else {
s.push(' ');
}
} else {
s.push(' ');
}
}
rows.push(s.trim_end().to_string());
}
rows
}
}
#[test]
fn hist_001_basic_insertion_no_wrap() {
// Screen of 20x6; viewport is the last row (height=1 at y=5)
let area = Rect::new(0, 5, 20, 1);
let mut scenario = TestScenario::new(20, 6, area);
let lines = vec![Line::from("first"), Line::from("second")];
let buf = scenario.run_insert(lines);
let rows = scenario.screen_rows_from_bytes(&buf);
assert_contains!(rows, String::from("first"));
assert_contains!(rows, String::from("second"));
let first_idx = rows
.iter()
.position(|r| r == "first")
.expect("expected 'first' row to be present");
let second_idx = rows
.iter()
.position(|r| r == "second")
.expect("expected 'second' row to be present");
assert_eq!(second_idx, first_idx + 1, "rows should be adjacent");
}
#[test]
fn hist_002_long_token_wraps() {
let area = Rect::new(0, 5, 20, 1);
let mut scenario = TestScenario::new(20, 6, area);
let long = "A".repeat(45); // > 2 lines at width 20
let lines = vec![Line::from(long.clone())];
let buf = scenario.run_insert(lines);
let mut parser = vt100::Parser::new(6, 20, 0);
parser.process(&buf);
let screen = parser.screen();
// Count total A's on the screen
let mut count_a = 0usize;
for row in 0..6 {
for col in 0..20 {
if let Some(cell) = screen.cell(row, col) {
if let Some(ch) = cell.contents().chars().next() {
if ch == 'A' {
count_a += 1;
}
}
}
}
}
assert_eq!(
count_a,
long.len(),
"wrapped content did not preserve all characters"
);
}
#[test]
fn hist_003_emoji_and_cjk() {
let area = Rect::new(0, 5, 20, 1);
let mut scenario = TestScenario::new(20, 6, area);
let text = String::from("😀😀😀😀😀 你好世界");
let lines = vec![Line::from(text.clone())];
let buf = scenario.run_insert(lines);
let rows = scenario.screen_rows_from_bytes(&buf);
let reconstructed: String = rows.join("").chars().filter(|c| *c != ' ').collect();
for ch in text.chars().filter(|c| !c.is_whitespace()) {
assert!(
reconstructed.contains(ch),
"missing character {ch:?} in reconstructed screen"
);
}
}
#[test]
fn hist_004_mixed_ansi_spans() {
let area = Rect::new(0, 5, 20, 1);
let mut scenario = TestScenario::new(20, 6, area);
let line = Line::from(vec![
Span::styled("red", Style::default().fg(Color::Red)),
Span::raw("+plain"),
]);
let buf = scenario.run_insert(vec![line]);
let rows = scenario.screen_rows_from_bytes(&buf);
assert_contains!(rows, String::from("red+plain"));
}
#[test]
fn hist_006_cursor_restoration() {
let area = Rect::new(0, 5, 20, 1);
let mut scenario = TestScenario::new(20, 6, area);
let lines = vec![Line::from("x")];
let buf = scenario.run_insert(lines);
let s = String::from_utf8_lossy(&buf);
// CUP to 1;1 (ANSI: ESC[1;1H)
assert!(
s.contains("\u{1b}[1;1H"),
"expected final CUP to 1;1 in output, got: {s:?}"
);
// Reset scroll region
assert!(
s.contains("\u{1b}[r"),
"expected reset scroll region in output, got: {s:?}"
);
}
#[test]
fn hist_005_pre_scroll_region_down() {
// Viewport not at bottom: y=3 (0-based), height=1
let area = Rect::new(0, 3, 20, 1);
let mut scenario = TestScenario::new(20, 6, area);
let lines = vec![Line::from("first"), Line::from("second")];
let buf = scenario.run_insert(lines);
let s = String::from_utf8_lossy(&buf);
// Expect we limited scroll region to [top+1 .. screen_height] => [4 .. 6] (1-based)
assert!(
s.contains("\u{1b}[4;6r"),
"expected pre-scroll SetScrollRegion 4..6, got: {s:?}"
);
// Expect we moved cursor to top of that region: row 3 (0-based) => CUP 4;1H
assert!(
s.contains("\u{1b}[4;1H"),
"expected cursor at top of pre-scroll region, got: {s:?}"
);
// Expect at least two Reverse Index commands (ESC M) for two inserted lines
let ri_count = s.matches("\u{1b}M").count();
assert!(
ri_count >= 1,
"expected at least one RI (ESC M), got: {s:?}"
);
// After pre-scroll, we set insertion scroll region to [1 .. new_top] => [1 .. 5]
assert!(
s.contains("\u{1b}[1;5r"),
"expected insertion SetScrollRegion 1..5, got: {s:?}"
);
}

View File

@@ -0,0 +1,101 @@
#![cfg(feature = "vt100-tests")]
use ratatui::backend::TestBackend;
use ratatui::layout::Rect;
use ratatui::text::Line;
#[test]
fn live_001_commit_on_overflow() {
let backend = TestBackend::new(20, 6);
let mut term = match codex_tui::custom_terminal::Terminal::with_options(backend) {
Ok(t) => t,
Err(e) => panic!("failed to construct terminal: {e}"),
};
let area = Rect::new(0, 5, 20, 1);
term.set_viewport_area(area);
// Build 5 explicit rows at width 20.
let mut rb = codex_tui::live_wrap::RowBuilder::new(20);
rb.push_fragment("one\n");
rb.push_fragment("two\n");
rb.push_fragment("three\n");
rb.push_fragment("four\n");
rb.push_fragment("five\n");
// Keep the last 3 in the live ring; commit the first 2.
let commit_rows = rb.drain_commit_ready(3);
let lines: Vec<Line<'static>> = commit_rows
.into_iter()
.map(|r| Line::from(r.text))
.collect();
let mut buf: Vec<u8> = Vec::new();
codex_tui::insert_history::insert_history_lines_to_writer(&mut term, &mut buf, lines);
let mut parser = vt100::Parser::new(6, 20, 0);
parser.process(&buf);
let screen = parser.screen();
// The words "one" and "two" should appear above the viewport.
let mut joined = String::new();
for row in 0..6 {
for col in 0..20 {
if let Some(cell) = screen.cell(row, col) {
if let Some(ch) = cell.contents().chars().next() {
joined.push(ch);
} else {
joined.push(' ');
}
}
}
joined.push('\n');
}
assert!(
joined.contains("one"),
"expected committed 'one' to be visible\n{joined}"
);
assert!(
joined.contains("two"),
"expected committed 'two' to be visible\n{joined}"
);
// The last three (three,four,five) remain in the live ring, not committed here.
}
#[test]
fn live_002_pre_scroll_and_commit() {
let backend = TestBackend::new(20, 6);
let mut term = match codex_tui::custom_terminal::Terminal::with_options(backend) {
Ok(t) => t,
Err(e) => panic!("failed to construct terminal: {e}"),
};
// Viewport not at bottom: y=3
let area = Rect::new(0, 3, 20, 1);
term.set_viewport_area(area);
let mut rb = codex_tui::live_wrap::RowBuilder::new(20);
rb.push_fragment("alpha\n");
rb.push_fragment("beta\n");
rb.push_fragment("gamma\n");
rb.push_fragment("delta\n");
// Keep 3, commit 1.
let commit_rows = rb.drain_commit_ready(3);
let lines: Vec<Line<'static>> = commit_rows
.into_iter()
.map(|r| Line::from(r.text))
.collect();
let mut buf: Vec<u8> = Vec::new();
codex_tui::insert_history::insert_history_lines_to_writer(&mut term, &mut buf, lines);
let s = String::from_utf8_lossy(&buf);
// Expect a SetScrollRegion to [area.top()+1 .. screen_height] and a cursor move to top of that region.
assert!(
s.contains("\u{1b}[4;6r"),
"expected pre-scroll region 4..6, got: {s:?}"
);
assert!(
s.contains("\u{1b}[4;1H"),
"expected cursor CUP 4;1H, got: {s:?}"
);
}