mirror of
https://github.com/openai/codex.git
synced 2026-10-31 19:28:49 +00:00
## Why Table holdback kept completed tables and subsequent content mutable until the response finished. Releasing completed tables earlier also requires a final scrollback reflow when late reference-link definitions change previously emitted content. ## What changed - Release table holdback once the containing top-level Markdown block is stable, rescanning the remaining tail for later tables. - Track completed-block boundaries across incremental and full renders. - Request scrollback reflow at answer completion when reference-link definitions are present, including definitions pending without a trailing newline. ## Testing Add regression assertions and snapshots for completed tables entering scrollback while later tables remain mutable, and for late reference-link definitions resolving correctly at completion. Cover reflow requests with and without a trailing newline and reference handling with inline visualizations enabled. GitOrigin-RevId: a6b2aac74496281495b53840bbb1daf06bef60d2
292 lines
11 KiB
Rust
292 lines
11 KiB
Rust
//! Incremental markdown rendering for in-flight transcript streams.
|
|
//!
|
|
//! Completed top-level blocks are retained while the final block stays mutable, avoiding repeated
|
|
//! rendering of the stable prefix as newline-bearing deltas arrive.
|
|
|
|
use super::code_fence::OpenCodeFence;
|
|
use crate::history_cell::HistoryRenderMode;
|
|
use crate::history_cell::raw_lines_from_source;
|
|
use crate::inline_visualization::InlineVisualizationContext;
|
|
use crate::inline_visualization::contains_inline_visualization;
|
|
use crate::markdown::render_markdown_agent_with_list_spacing;
|
|
use crate::markdown::render_streaming_markdown_agent_with_links_and_cwd;
|
|
use crate::markdown_render::ListSpacing;
|
|
use crate::render::highlight::syntax_theme_revision;
|
|
use crate::terminal_hyperlinks::HyperlinkLine;
|
|
use crate::terminal_hyperlinks::plain_hyperlink_lines;
|
|
use ratatui::text::Line;
|
|
use std::path::Path;
|
|
|
|
/// Incremental render state split at source and rendered-line boundaries.
|
|
///
|
|
/// The prefix before both boundaries is immutable; only the final top-level Markdown block is
|
|
/// re-rendered as committed source arrives.
|
|
pub(super) struct StreamingRender {
|
|
pub(super) list_spacing: ListSpacing,
|
|
pub(super) lines: Vec<HyperlinkLine>,
|
|
pub(super) pending_math_start: Option<usize>,
|
|
pub(super) mutable_fence_start: Option<usize>,
|
|
/// Source boundary before the final top-level block, including after full re-renders.
|
|
pub(super) completed_source_len: usize,
|
|
/// Source prefix containing only completed top-level markdown blocks.
|
|
stable_source_len: usize,
|
|
/// Rendered-line boundary corresponding to `stable_source_len`.
|
|
stable_rendered_len: usize,
|
|
/// Reference-style link definitions can affect any earlier or later markdown block.
|
|
pub(super) has_reference_link_definition: bool,
|
|
/// Inline visualization directives require source-wide rewriting once one is committed.
|
|
has_inline_visualization_directive: bool,
|
|
/// Parser state for a directly appendable, open top-level code fence.
|
|
open_code_fence: Option<OpenCodeFence>,
|
|
}
|
|
|
|
impl StreamingRender {
|
|
pub(super) fn new() -> Self {
|
|
Self {
|
|
list_spacing: ListSpacing::AfterMultiline,
|
|
lines: Vec::with_capacity(64),
|
|
pending_math_start: None,
|
|
mutable_fence_start: None,
|
|
completed_source_len: 0,
|
|
stable_source_len: 0,
|
|
stable_rendered_len: 0,
|
|
has_reference_link_definition: false,
|
|
has_inline_visualization_directive: false,
|
|
open_code_fence: None,
|
|
}
|
|
}
|
|
|
|
pub(super) fn clear(&mut self) {
|
|
self.lines.clear();
|
|
self.pending_math_start = None;
|
|
self.mutable_fence_start = None;
|
|
self.completed_source_len = 0;
|
|
self.stable_source_len = 0;
|
|
self.stable_rendered_len = 0;
|
|
self.has_reference_link_definition = false;
|
|
self.has_inline_visualization_directive = false;
|
|
self.open_code_fence = None;
|
|
}
|
|
|
|
/// Re-render the full source and reset both stable-prefix boundaries.
|
|
///
|
|
/// This is used when width or render mode changes, and whenever source-wide rendering state
|
|
/// makes retaining previously rendered blocks unsafe.
|
|
pub(super) fn recompute(
|
|
&mut self,
|
|
source: &str,
|
|
width: Option<usize>,
|
|
cwd: &Path,
|
|
render_mode: HistoryRenderMode,
|
|
inline_visualization_context: Option<&InlineVisualizationContext>,
|
|
) {
|
|
self.open_code_fence = None;
|
|
self.pending_math_start = None;
|
|
self.mutable_fence_start = None;
|
|
self.completed_source_len = 0;
|
|
self.has_inline_visualization_directive = contains_inline_visualization(source);
|
|
self.lines = match (render_mode, inline_visualization_context) {
|
|
(HistoryRenderMode::Rich, None) if !self.has_inline_visualization_directive => {
|
|
let rendered = render_streaming_markdown_agent_with_links_and_cwd(
|
|
source,
|
|
width,
|
|
Some(cwd),
|
|
self.list_spacing,
|
|
);
|
|
self.has_reference_link_definition = rendered.has_reference_link_definition;
|
|
self.completed_source_len = rendered.last_top_level_block_start.unwrap_or(0);
|
|
self.pending_math_start = rendered.pending_math_start;
|
|
self.mutable_fence_start = rendered.mutable_fence_start;
|
|
rendered.lines
|
|
}
|
|
_ => {
|
|
self.has_reference_link_definition = false;
|
|
if render_mode == HistoryRenderMode::Rich {
|
|
let rendered = render_streaming_markdown_agent_with_links_and_cwd(
|
|
source,
|
|
width,
|
|
Some(cwd),
|
|
self.list_spacing,
|
|
);
|
|
self.has_reference_link_definition = rendered.has_reference_link_definition;
|
|
self.completed_source_len = rendered.last_top_level_block_start.unwrap_or(0);
|
|
self.pending_math_start = rendered.pending_math_start;
|
|
self.mutable_fence_start = rendered.mutable_fence_start;
|
|
}
|
|
render_source_with_list_spacing(
|
|
source,
|
|
width,
|
|
cwd,
|
|
render_mode,
|
|
inline_visualization_context,
|
|
self.list_spacing,
|
|
)
|
|
}
|
|
};
|
|
self.stable_source_len = 0;
|
|
self.stable_rendered_len = 0;
|
|
}
|
|
|
|
/// Append newly committed source while retaining only the final markdown block as mutable.
|
|
///
|
|
/// The final top-level block can still change meaning when another line arrives (for example,
|
|
/// list tightness, a setext heading, a fenced block, or a table). Earlier top-level blocks are
|
|
/// rendered once and retained. Reference-style link definitions and inline visualization
|
|
/// rewriting fall back to a full render because they can affect source-wide rendering state.
|
|
pub(super) fn append(
|
|
&mut self,
|
|
raw_source: &str,
|
|
committed_source: &str,
|
|
width: Option<usize>,
|
|
cwd: &Path,
|
|
render_mode: HistoryRenderMode,
|
|
inline_visualization_context: Option<&InlineVisualizationContext>,
|
|
) {
|
|
if render_mode == HistoryRenderMode::Raw {
|
|
self.lines
|
|
.extend(plain_hyperlink_lines(raw_lines_from_source(
|
|
committed_source,
|
|
)));
|
|
return;
|
|
}
|
|
|
|
self.has_inline_visualization_directive |= contains_inline_visualization(committed_source);
|
|
if self.has_inline_visualization_directive {
|
|
self.recompute(
|
|
raw_source,
|
|
width,
|
|
cwd,
|
|
render_mode,
|
|
inline_visualization_context,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if self.has_reference_link_definition {
|
|
self.recompute(
|
|
raw_source,
|
|
width,
|
|
cwd,
|
|
render_mode,
|
|
inline_visualization_context,
|
|
);
|
|
return;
|
|
}
|
|
|
|
if let Some(fence) = self.open_code_fence.take()
|
|
&& let Some((fence, lines)) = fence.append(raw_source, committed_source)
|
|
{
|
|
self.lines.extend(lines);
|
|
self.open_code_fence = Some(fence);
|
|
return;
|
|
}
|
|
|
|
let pending_source = &raw_source[self.stable_source_len..];
|
|
let theme_revision = syntax_theme_revision();
|
|
let pending = render_streaming_markdown_agent_with_links_and_cwd(
|
|
pending_source,
|
|
width,
|
|
Some(cwd),
|
|
self.list_spacing,
|
|
);
|
|
self.pending_math_start = pending
|
|
.pending_math_start
|
|
.map(|start| self.stable_source_len + start);
|
|
self.mutable_fence_start = pending
|
|
.mutable_fence_start
|
|
.map(|start| self.stable_source_len + start);
|
|
if pending.has_reference_link_definition {
|
|
self.has_reference_link_definition = true;
|
|
self.recompute(
|
|
raw_source,
|
|
width,
|
|
cwd,
|
|
render_mode,
|
|
inline_visualization_context,
|
|
);
|
|
return;
|
|
}
|
|
|
|
let final_block_start = pending.last_top_level_block_start.unwrap_or(/*default*/ 0);
|
|
self.completed_source_len = self.stable_source_len + final_block_start;
|
|
self.open_code_fence = OpenCodeFence::detect(
|
|
&pending_source[final_block_start..],
|
|
raw_source.len(),
|
|
theme_revision,
|
|
);
|
|
|
|
let mut newly_stable_rendered_len = None;
|
|
if let Some(boundary) = pending.last_top_level_block_start {
|
|
let newly_stable_source = &pending_source[..boundary];
|
|
let newly_stable = render_source_with_list_spacing(
|
|
newly_stable_source,
|
|
width,
|
|
cwd,
|
|
render_mode,
|
|
inline_visualization_context,
|
|
self.list_spacing,
|
|
);
|
|
self.stable_source_len += boundary;
|
|
newly_stable_rendered_len = Some(newly_stable.len());
|
|
}
|
|
|
|
self.lines.truncate(self.stable_rendered_len);
|
|
if !self.lines.is_empty()
|
|
&& (!pending.lines.is_empty() || !pending_source.trim().is_empty())
|
|
&& !pending.first_top_level_block_is_html
|
|
{
|
|
self.lines.push(HyperlinkLine::new(Line::default()));
|
|
}
|
|
let pending_render_start = self.lines.len();
|
|
self.lines.extend(pending.lines);
|
|
if let Some(newly_stable_rendered_len) = newly_stable_rendered_len {
|
|
self.stable_rendered_len = pending_render_start + newly_stable_rendered_len;
|
|
}
|
|
}
|
|
}
|
|
|
|
pub(super) fn render_source(
|
|
source: &str,
|
|
width: Option<usize>,
|
|
cwd: &Path,
|
|
render_mode: HistoryRenderMode,
|
|
inline_visualization_context: Option<&InlineVisualizationContext>,
|
|
) -> Vec<HyperlinkLine> {
|
|
render_source_with_list_spacing(
|
|
source,
|
|
width,
|
|
cwd,
|
|
render_mode,
|
|
inline_visualization_context,
|
|
ListSpacing::AfterMultiline,
|
|
)
|
|
}
|
|
|
|
pub(super) fn render_source_with_list_spacing(
|
|
source: &str,
|
|
width: Option<usize>,
|
|
cwd: &Path,
|
|
render_mode: HistoryRenderMode,
|
|
inline_visualization_context: Option<&InlineVisualizationContext>,
|
|
list_spacing: ListSpacing,
|
|
) -> Vec<HyperlinkLine> {
|
|
match render_mode {
|
|
HistoryRenderMode::Rich => render_markdown_agent_with_list_spacing(
|
|
source,
|
|
width,
|
|
Some(cwd),
|
|
inline_visualization_context,
|
|
list_spacing,
|
|
),
|
|
HistoryRenderMode::Raw => plain_hyperlink_lines(raw_lines_from_source(source)),
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
#[path = "render_tests.rs"]
|
|
mod tests;
|
|
|
|
#[cfg(test)]
|
|
#[path = "inline_math_tests.rs"]
|
|
mod inline_math_tests;
|