mirror of
https://github.com/openai/codex.git
synced 2026-05-17 09:43:19 +00:00
fix(tui): preserve streamed markdown order
This commit is contained in:
@@ -4289,10 +4289,19 @@ impl ChatWidget {
|
||||
scope,
|
||||
now,
|
||||
);
|
||||
let emitted_cells = !outcome.cells.is_empty();
|
||||
for cell in outcome.cells {
|
||||
self.bottom_pane.hide_status_indicator();
|
||||
self.add_boxed_history(cell);
|
||||
}
|
||||
if emitted_cells {
|
||||
if self.stream_controller.is_some() {
|
||||
self.sync_agent_stream_active_tail();
|
||||
}
|
||||
if self.plan_stream_controller.is_some() {
|
||||
self.sync_plan_stream_active_tail();
|
||||
}
|
||||
}
|
||||
|
||||
if outcome.has_controller && outcome.all_idle {
|
||||
self.maybe_restore_status_indicator_after_stream_idle();
|
||||
|
||||
@@ -106,8 +106,22 @@ pub(super) fn normalize_table_boundaries(input: &str) -> Cow<'_, str> {
|
||||
let mut out = String::with_capacity(input.len());
|
||||
let mut changed = false;
|
||||
let mut index = 0;
|
||||
let mut code_fence: Option<(char, usize)> = None;
|
||||
while index < lines.len() {
|
||||
if index + 1 < lines.len()
|
||||
if let Some(fence) = code_fence {
|
||||
out.push_str(lines[index]);
|
||||
if is_closing_code_fence(lines[index], fence) {
|
||||
code_fence = None;
|
||||
}
|
||||
index += 1;
|
||||
} else if let Some(fence) = opening_code_fence(lines[index]) {
|
||||
code_fence = Some(fence);
|
||||
out.push_str(lines[index]);
|
||||
index += 1;
|
||||
} else if is_indented_code_line(lines[index]) {
|
||||
out.push_str(lines[index]);
|
||||
index += 1;
|
||||
} else if index + 1 < lines.len()
|
||||
&& is_table_row_source(lines[index])
|
||||
&& is_table_delimiter_source(lines[index + 1])
|
||||
{
|
||||
@@ -137,6 +151,47 @@ pub(super) fn normalize_table_boundaries(input: &str) -> Cow<'_, str> {
|
||||
}
|
||||
}
|
||||
|
||||
fn opening_code_fence(line: &str) -> Option<(char, usize)> {
|
||||
let trimmed = strip_fence_indent(line)?;
|
||||
let mut chars = trimmed.chars();
|
||||
let marker = chars.next()?;
|
||||
if marker != '`' && marker != '~' {
|
||||
return None;
|
||||
}
|
||||
|
||||
let marker_count = 1 + chars.take_while(|ch| *ch == marker).count();
|
||||
(marker_count >= 3).then_some((marker, marker_count))
|
||||
}
|
||||
|
||||
fn is_closing_code_fence(line: &str, (marker, opening_count): (char, usize)) -> bool {
|
||||
let Some(trimmed) = strip_fence_indent(line) else {
|
||||
return false;
|
||||
};
|
||||
let marker_count = trimmed.chars().take_while(|ch| *ch == marker).count();
|
||||
marker_count >= opening_count
|
||||
&& trimmed[marker.len_utf8() * marker_count..]
|
||||
.trim()
|
||||
.is_empty()
|
||||
}
|
||||
|
||||
fn strip_fence_indent(line: &str) -> Option<&str> {
|
||||
let mut spaces = 0usize;
|
||||
for (index, ch) in line.char_indices() {
|
||||
if ch != ' ' {
|
||||
return (spaces <= 3).then_some(&line[index..]);
|
||||
}
|
||||
spaces += 1;
|
||||
if spaces > 3 {
|
||||
return None;
|
||||
}
|
||||
}
|
||||
Some("")
|
||||
}
|
||||
|
||||
fn is_indented_code_line(line: &str) -> bool {
|
||||
line.starts_with(" ") || line.starts_with('\t')
|
||||
}
|
||||
|
||||
fn is_table_row_source(line: &str) -> bool {
|
||||
let trimmed = line.trim();
|
||||
!trimmed.is_empty() && trimmed.contains('|')
|
||||
|
||||
@@ -127,6 +127,38 @@ fn table_inline_links_and_html_breaks_stay_inside_table() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_boundary_normalization_does_not_mutate_code_blocks() {
|
||||
let markdown = "```\n| A | B |\n| --- | --- |\n```\nAfter.\n";
|
||||
let rendered = render_markdown_text(markdown);
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
Text::from_iter([
|
||||
Line::from_iter(["", "| A | B |"]),
|
||||
Line::from_iter(["", "| --- | --- |"]),
|
||||
Line::default(),
|
||||
Line::from("After."),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn table_boundary_normalization_does_not_mutate_indented_code_blocks() {
|
||||
let markdown = " | A | B |\n | --- | --- |\nAfter.\n";
|
||||
let rendered = render_markdown_text(markdown);
|
||||
|
||||
assert_eq!(
|
||||
rendered,
|
||||
Text::from_iter([
|
||||
Line::from_iter([" ", "| A | B |"]),
|
||||
Line::from_iter([" ", "| --- | --- |"]),
|
||||
Line::default(),
|
||||
Line::from("After."),
|
||||
]),
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty() {
|
||||
assert_eq!(render_markdown_text(""), Text::default());
|
||||
|
||||
@@ -290,6 +290,9 @@ impl StreamController {
|
||||
}
|
||||
|
||||
pub(crate) fn active_tail_cell(&self) -> Option<Box<dyn HistoryCell>> {
|
||||
if self.core.queued_lines() > 0 {
|
||||
return None;
|
||||
}
|
||||
let lines = self.core.active_tail_lines();
|
||||
if lines.is_empty() {
|
||||
return None;
|
||||
@@ -396,6 +399,9 @@ impl PlanStreamController {
|
||||
}
|
||||
|
||||
pub(crate) fn active_tail_cell(&self) -> Option<Box<dyn HistoryCell>> {
|
||||
if self.core.queued_lines() > 0 {
|
||||
return None;
|
||||
}
|
||||
let lines = self.core.active_tail_lines();
|
||||
if lines.is_empty() {
|
||||
return None;
|
||||
@@ -675,10 +681,79 @@ mod tests {
|
||||
ctrl.queued_lines() > 0,
|
||||
"table should enter stable queue after a later block appears"
|
||||
);
|
||||
assert_eq!(
|
||||
active_tail_plain_strings(&ctrl),
|
||||
Vec::<String>::new(),
|
||||
"later block should wait behind the queued stable table",
|
||||
);
|
||||
|
||||
let (_cell, idle) = ctrl.on_commit_tick_batch(/*max_lines*/ usize::MAX);
|
||||
assert!(idle);
|
||||
let new_tail = active_tail_plain_strings(&ctrl);
|
||||
assert!(
|
||||
new_tail.iter().any(|line| line.contains("After table.")),
|
||||
"later block should become new active tail: {new_tail:?}",
|
||||
"later block should become active tail after queued table drains: {new_tail:?}",
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn active_tail_waits_for_queued_stable_blocks() {
|
||||
let mut ctrl = stream_controller(/*width*/ Some(80));
|
||||
|
||||
assert!(ctrl.push("first\n\nsecond\n"));
|
||||
|
||||
assert_eq!(
|
||||
active_tail_plain_strings(&ctrl),
|
||||
Vec::<String>::new(),
|
||||
"new tail must not render ahead of queued stable content",
|
||||
);
|
||||
|
||||
let (cell, idle) = ctrl.on_commit_tick();
|
||||
let emitted = lines_to_plain_strings(
|
||||
&cell
|
||||
.expect("expected queued stable block to emit first")
|
||||
.transcript_lines(u16::MAX),
|
||||
);
|
||||
assert_eq!(emitted, vec!["• first"]);
|
||||
assert!(idle);
|
||||
|
||||
assert_eq!(active_tail_plain_strings(&ctrl), vec![" second"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn plan_active_tail_waits_for_queued_stable_blocks() {
|
||||
let mut ctrl = plan_stream_controller(/*width*/ Some(80));
|
||||
|
||||
assert!(ctrl.push("first\n\nsecond\n"));
|
||||
|
||||
assert!(
|
||||
ctrl.active_tail_cell().is_none(),
|
||||
"new plan tail must not render ahead of queued stable content",
|
||||
);
|
||||
|
||||
let (cell, idle) = ctrl.on_commit_tick();
|
||||
let emitted = lines_to_plain_strings(
|
||||
&cell
|
||||
.expect("expected queued stable plan block to emit first")
|
||||
.transcript_lines(u16::MAX),
|
||||
);
|
||||
assert!(
|
||||
emitted.iter().any(|line| line.contains("Proposed Plan"))
|
||||
&& emitted.iter().any(|line| line.contains("first")),
|
||||
"first plan block should emit before active tail: {emitted:?}",
|
||||
);
|
||||
assert!(idle);
|
||||
|
||||
let tail = lines_to_plain_strings(
|
||||
&ctrl
|
||||
.active_tail_cell()
|
||||
.expect("expected active tail after queue drains")
|
||||
.transcript_lines(u16::MAX),
|
||||
);
|
||||
assert!(
|
||||
tail.iter().all(|line| !line.contains("Proposed Plan"))
|
||||
&& tail.iter().any(|line| line.contains("second")),
|
||||
"tail should continue the existing plan cell without a duplicate header: {tail:?}",
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user