From c0fb777ed7c7950c6a2f397113c1842c2e679306 Mon Sep 17 00:00:00 2001 From: Ogulcan Celik Date: Fri, 24 Jul 2026 04:00:13 +0300 Subject: fix: report truncated pane reads refs #1717 --- src/app/api/agents.rs | 6 ++-- src/app/api/panes.rs | 28 +++++++++++++++-- src/app/api_helpers.rs | 83 +++++++++++++++++++++++++++++++++----------------- 3 files changed, 83 insertions(+), 34 deletions(-) (limited to 'src/app') diff --git a/src/app/api/agents.rs b/src/app/api/agents.rs index ffb9cb63..f1977ce4 100644 --- a/src/app/api/agents.rs +++ b/src/app/api/agents.rs @@ -117,7 +117,7 @@ impl App { else { return agent_not_found(id, ¶ms.target); }; - let text = crate::app::api_helpers::read_terminal_snapshot( + let snapshot = crate::app::api_helpers::read_terminal_snapshot( pane, params.source, params.format, @@ -137,9 +137,9 @@ impl App { .unwrap(), source: params.source, format: params.format, - text, + text: snapshot.text, revision: 0, - truncated: false, + truncated: snapshot.truncated, }, }, ) diff --git a/src/app/api/panes.rs b/src/app/api/panes.rs index 43206d1e..253fd627 100644 --- a/src/app/api/panes.rs +++ b/src/app/api/panes.rs @@ -1179,7 +1179,7 @@ impl App { else { return pane_not_found(id, ¶ms.pane_id); }; - let text = crate::app::api_helpers::read_terminal_snapshot( + let snapshot = crate::app::api_helpers::read_terminal_snapshot( pane, params.source, params.format, @@ -1195,9 +1195,9 @@ impl App { tab_id: self.public_tab_id(ws_idx, tab_idx).unwrap(), source: params.source, format: params.format, - text, + text: snapshot.text, revision: 0, - truncated: false, + truncated: snapshot.truncated, }, }, ) @@ -2001,6 +2001,28 @@ mod tests { assert_eq!(scroll.viewport_rows, 5); } + #[tokio::test] + async fn api_pane_read_reports_when_older_rows_are_omitted() { + let (mut app, public_pane_id, _pane_id) = app_with_scrollback_runtime(); + + let response = app.handle_pane_read( + "req".into(), + PaneReadParams { + pane_id: public_pane_id, + source: crate::api::schema::ReadSource::Recent, + lines: Some(2), + format: crate::api::schema::ReadFormat::Text, + strip_ansi: true, + }, + ); + let success: SuccessResponse = serde_json::from_str(&response).unwrap(); + let ResponseResult::PaneRead { read } = success.result else { + panic!("expected pane read response"); + }; + assert!(read.text.contains("line 19")); + assert!(read.truncated); + } + #[tokio::test] async fn api_pane_send_keys_preserves_legacy_control_c_aliases() { let (mut app, pane_id, mut rx) = app_with_send_key_runtime(3); diff --git a/src/app/api_helpers.rs b/src/app/api_helpers.rs index 6c393dac..e0091b39 100644 --- a/src/app/api_helpers.rs +++ b/src/app/api_helpers.rs @@ -106,50 +106,77 @@ pub(super) fn read_terminal_snapshot( source: crate::api::schema::ReadSource, format: crate::api::schema::ReadFormat, lines: Option, -) -> String { +) -> crate::pane::TerminalReadSnapshot { use crate::api::schema::{ReadFormat, ReadSource}; - let requested_lines = lines.unwrap_or(80).min(1000) as usize; - let text = match (format, source) { - (ReadFormat::Text, ReadSource::Visible) => terminal.visible_text(), - (ReadFormat::Text, ReadSource::Recent) => terminal.recent_text(requested_lines), + let line_limit = lines.map(|lines| lines.min(1000) as usize); + let recent_lines = line_limit.unwrap_or(80); + match (format, source) { + (ReadFormat::Text, ReadSource::Visible) => { + limit_snapshot_lines(terminal.visible_text(), line_limit) + } + (ReadFormat::Text, ReadSource::Recent) => terminal.recent_text_snapshot(recent_lines), (ReadFormat::Text, ReadSource::RecentUnwrapped) => { - terminal.recent_unwrapped_text(requested_lines) + terminal.recent_unwrapped_text_snapshot(recent_lines) + } + (ReadFormat::Text, ReadSource::Detection) => { + limit_snapshot_lines(terminal.detection_text(), line_limit) + } + (ReadFormat::Ansi, ReadSource::Visible) => { + limit_snapshot_lines(terminal.visible_ansi(), line_limit) } - (ReadFormat::Text, ReadSource::Detection) => terminal.detection_text(), - (ReadFormat::Ansi, ReadSource::Visible) => terminal.visible_ansi(), - (ReadFormat::Ansi, ReadSource::Recent) => terminal.recent_ansi(requested_lines), + (ReadFormat::Ansi, ReadSource::Recent) => terminal.recent_ansi_snapshot(recent_lines), (ReadFormat::Ansi, ReadSource::RecentUnwrapped) => { - terminal.recent_unwrapped_ansi(requested_lines) + terminal.recent_unwrapped_ansi_snapshot(recent_lines) + } + (ReadFormat::Ansi, ReadSource::Detection) => { + limit_snapshot_lines(terminal.detection_text(), line_limit) } - (ReadFormat::Ansi, ReadSource::Detection) => terminal.detection_text(), - }; - - if lines.is_some() && matches!(source, ReadSource::Visible | ReadSource::Detection) { - last_lines(&text, requested_lines) - } else { - text } } -fn last_lines(text: &str, count: usize) -> String { - if text.is_empty() || count == 0 { - return String::new(); - } +fn limit_snapshot_lines(text: String, limit: Option) -> crate::pane::TerminalReadSnapshot { + let Some(limit) = limit else { + return crate::pane::TerminalReadSnapshot { + text, + truncated: false, + }; + }; let lines: Vec<_> = text.split_inclusive('\n').collect(); - lines[lines.len().saturating_sub(count)..].concat() + crate::pane::TerminalReadSnapshot { + text: lines[lines.len().saturating_sub(limit)..].concat(), + truncated: lines.len() > limit, + } } #[cfg(test)] mod read_snapshot_tests { - use super::last_lines; + use super::limit_snapshot_lines; + + #[test] + fn line_limit_preserves_endings_and_reports_omitted_lines() { + let snapshot = limit_snapshot_lines("one\ntwø\n三\n".into(), Some(2)); + assert_eq!(snapshot.text, "twø\n三\n"); + assert!(snapshot.truncated); + + let snapshot = limit_snapshot_lines("one\ntwo\nthree".into(), Some(1)); + assert_eq!(snapshot.text, "three"); + assert!(snapshot.truncated); + + let snapshot = limit_snapshot_lines("one\ntwo".into(), Some(0)); + assert_eq!(snapshot.text, ""); + assert!(snapshot.truncated); + + let snapshot = limit_snapshot_lines("".into(), Some(2)); + assert_eq!(snapshot.text, ""); + assert!(!snapshot.truncated); + } #[test] - fn last_lines_preserves_real_line_endings_and_unicode() { - assert_eq!(last_lines("one\ntwø\n三\n", 2), "twø\n三\n"); - assert_eq!(last_lines("one\ntwo\nthree", 1), "three"); - assert_eq!(last_lines("one\ntwo", 0), ""); - assert_eq!(last_lines("", 2), ""); + fn omitted_line_limit_returns_the_complete_snapshot() { + let snapshot = limit_snapshot_lines("one\ntwo\n".into(), None); + assert_eq!(snapshot.text, "one\ntwo\n"); + assert!(!snapshot.truncated); } } -- cgit v1.2.3