Fix output width issue

This commit is contained in:
canvrno-oai
2026-04-08 12:30:30 -07:00
parent e003f84e1e
commit bbf2554b98
5 changed files with 103 additions and 26 deletions
+15 -5
View File
@@ -3985,10 +3985,7 @@ impl App {
event: TuiEvent,
) -> Result<AppRunControl> {
if matches!(event, TuiEvent::Draw) {
let size = tui.terminal.size()?;
if size != tui.terminal.last_known_screen_size {
self.refresh_status_line();
}
self.sync_terminal_size_dependent_state(tui)?;
}
if self.overlay.is_some() {
@@ -4040,6 +4037,15 @@ impl App {
Ok(AppRunControl::Continue)
}
fn sync_terminal_size_dependent_state(&mut self, tui: &mut tui::Tui) -> Result<()> {
let size = tui.terminal.size()?;
if size != tui.terminal.last_known_screen_size {
self.chat_widget.update_stream_render_width(size.width);
self.refresh_status_line();
}
Ok(())
}
async fn handle_event(
&mut self,
tui: &mut tui::Tui,
@@ -4256,7 +4262,8 @@ impl App {
tui.frame_requester().schedule_frame();
}
self.transcript_cells.push(cell.clone());
let mut display = cell.display_lines(tui.terminal.last_known_screen_size.width);
let display_width = tui.terminal.size()?.width;
let mut display = cell.display_lines(display_width);
if !display.is_empty() {
// Only insert a separating blank line for new cells that are not
// part of an ongoing stream. Streaming continuations should not
@@ -4300,6 +4307,7 @@ impl App {
self.commit_anim_running.store(false, Ordering::Release);
}
AppEvent::CommitTick => {
self.sync_terminal_size_dependent_state(tui)?;
self.chat_widget.on_commit_tick();
}
AppEvent::Exit(mode) => {
@@ -5709,6 +5717,8 @@ impl App {
app_server: &mut AppServerSession,
event: ThreadBufferedEvent,
) -> Result<()> {
self.sync_terminal_size_dependent_state(tui)?;
// Capture this before any potential thread switch: we only want to clear
// the exit marker when the currently active thread acknowledges shutdown.
let pending_shutdown_exit_completed = matches!(
+10
View File
@@ -4026,6 +4026,16 @@ impl ChatWidget {
}
}
pub(crate) fn update_stream_render_width(&mut self, terminal_width: u16) {
let terminal_width = usize::from(terminal_width);
if let Some(controller) = self.stream_controller.as_mut() {
controller.set_width(Some(terminal_width.saturating_sub(2)));
}
if let Some(controller) = self.plan_stream_controller.as_mut() {
controller.set_width(Some(terminal_width.saturating_sub(4)));
}
}
/// Handle completion of an `AgentMessage` turn item.
///
/// Commentary completion sets a deferred restore flag so the status row
+64 -21
View File
@@ -3,12 +3,14 @@ use std::path::Path;
use std::path::PathBuf;
use crate::markdown;
use crate::render::line_utils::is_blank_line_spaces_only;
/// Newline-gated accumulator that renders markdown and commits only fully
/// completed logical lines.
pub(crate) struct MarkdownStreamCollector {
buffer: String,
committed_line_count: usize,
committed_source_len: usize,
width: Option<usize>,
cwd: PathBuf,
}
@@ -24,6 +26,7 @@ impl MarkdownStreamCollector {
Self {
buffer: String::new(),
committed_line_count: 0,
committed_source_len: 0,
width,
cwd: cwd.to_path_buf(),
}
@@ -32,6 +35,18 @@ impl MarkdownStreamCollector {
pub fn clear(&mut self) {
self.buffer.clear();
self.committed_line_count = 0;
self.committed_source_len = 0;
}
pub fn set_width(&mut self, width: Option<usize>) {
if self.width == width {
return;
}
self.width = width;
// The rendered line index is width-dependent; preserve the source commit boundary and
// recompute how many rendered lines it occupies at the new width.
self.committed_line_count = self.rendered_committed_line_count();
}
pub fn push_delta(&mut self, delta: &str) {
@@ -43,35 +58,37 @@ impl MarkdownStreamCollector {
/// since the last commit. When the buffer does not end with a newline, the
/// final rendered line is considered incomplete and is not emitted.
pub fn commit_complete_lines(&mut self) -> Vec<Line<'static>> {
let source = self.buffer.clone();
let last_newline_idx = source.rfind('\n');
let source = if let Some(last_newline_idx) = last_newline_idx {
source[..=last_newline_idx].to_string()
} else {
let Some(last_newline_idx) = self.buffer.rfind('\n') else {
return Vec::new();
};
let commit_source_len = last_newline_idx + 1;
let source = &self.buffer[..commit_source_len];
let mut rendered: Vec<Line<'static>> = Vec::new();
markdown::append_markdown(&source, self.width, Some(self.cwd.as_path()), &mut rendered);
let mut complete_line_count = rendered.len();
if complete_line_count > 0
&& crate::render::line_utils::is_blank_line_spaces_only(
&rendered[complete_line_count - 1],
)
{
complete_line_count -= 1;
}
markdown::append_markdown(source, self.width, Some(self.cwd.as_path()), &mut rendered);
let complete_line_count = complete_line_count(&rendered);
if self.committed_line_count >= complete_line_count {
return Vec::new();
}
let out = if self.committed_line_count >= complete_line_count {
Vec::new()
} else {
rendered[self.committed_line_count..complete_line_count].to_vec()
};
let out_slice = &rendered[self.committed_line_count..complete_line_count];
let out = out_slice.to_vec();
self.committed_line_count = complete_line_count;
self.committed_line_count = complete_line_count.max(self.committed_line_count);
self.committed_source_len = commit_source_len;
out
}
fn rendered_committed_line_count(&self) -> usize {
if self.committed_source_len == 0 {
return 0;
}
let source = &self.buffer[..self.committed_source_len];
let mut rendered: Vec<Line<'static>> = Vec::new();
markdown::append_markdown(source, self.width, Some(self.cwd.as_path()), &mut rendered);
complete_line_count(&rendered)
}
/// Finalize the stream: emit all remaining lines beyond the last commit.
/// If the buffer does not end with a newline, a temporary one is appended
/// for rendering. Optionally unwraps ```markdown language fences in
@@ -106,6 +123,14 @@ impl MarkdownStreamCollector {
}
}
fn complete_line_count(rendered: &[Line<'static>]) -> usize {
let mut complete_line_count = rendered.len();
if complete_line_count > 0 && is_blank_line_spaces_only(&rendered[complete_line_count - 1]) {
complete_line_count -= 1;
}
complete_line_count
}
#[cfg(test)]
fn test_cwd() -> PathBuf {
// These tests only need a stable absolute cwd; using temp_dir() avoids baking Unix- or
@@ -156,6 +181,24 @@ mod tests {
assert_eq!(out.len(), 1);
}
#[tokio::test]
async fn changing_width_preserves_source_commit_position() {
let mut c = super::MarkdownStreamCollector::new(Some(16), &super::test_cwd());
c.push_delta("- first second third fourth\n");
let first = c.commit_complete_lines();
assert_eq!(
lines_to_plain_strings(&first),
vec!["- first second", " third fourth"]
);
c.set_width(Some(40));
c.push_delta("- fifth sixth\n");
let next = c.commit_complete_lines();
assert_eq!(lines_to_plain_strings(&next), vec!["- fifth sixth"]);
}
#[tokio::test]
async fn e2e_stream_blockquote_simple_is_green() {
let out = super::simulate_stream_markdown_for_tests(&["> Hello\n"], /*finalize*/ true);
+10
View File
@@ -100,6 +100,11 @@ impl StreamController {
self.state.oldest_queued_age(now)
}
/// Updates the markdown render width used for future stream commits.
pub(crate) fn set_width(&mut self, width: Option<usize>) {
self.state.set_width(width);
}
fn emit(&mut self, lines: Vec<Line<'static>>) -> Option<Box<dyn HistoryCell>> {
if lines.is_empty() {
return None;
@@ -204,6 +209,11 @@ impl PlanStreamController {
self.state.oldest_queued_age(now)
}
/// Updates the markdown render width used for future stream commits.
pub(crate) fn set_width(&mut self, width: Option<usize>) {
self.state.set_width(width);
}
fn emit(
&mut self,
lines: Vec<Line<'static>>,
+4
View File
@@ -51,6 +51,10 @@ impl StreamState {
self.queued_lines.clear();
self.has_seen_delta = false;
}
/// Updates the markdown render width used for future stream commits.
pub(crate) fn set_width(&mut self, width: Option<usize>) {
self.collector.set_width(width);
}
/// Drains one queued line from the front of the queue.
pub(crate) fn step(&mut self) -> Vec<Line<'static>> {
self.queued_lines