|
1 | 1 | use anstyle::{AnsiColor, Color, Style}; |
2 | 2 | use anyhow::{Context, Result}; |
3 | 3 | use serde_json::Value; |
| 4 | +use std::collections::{HashMap, VecDeque}; |
4 | 5 | use vtcode_core::config::ToolOutputMode; |
5 | 6 | use vtcode_core::config::constants::{defaults, tools}; |
6 | 7 | use vtcode_core::config::loader::VTCodeConfig; |
@@ -30,16 +31,17 @@ pub(crate) fn render_tool_output( |
30 | 31 | } |
31 | 32 | } |
32 | 33 |
|
| 34 | + let output_mode = vt_config |
| 35 | + .map(|cfg| cfg.ui.tool_output_mode) |
| 36 | + .unwrap_or(ToolOutputMode::Compact); |
| 37 | + let tail_limit = resolve_stdout_tail_limit(vt_config); |
| 38 | + |
33 | 39 | if tool_name == Some(tools::CURL) { |
34 | | - render_curl_result(renderer, val)?; |
| 40 | + render_curl_result(renderer, val, output_mode, tail_limit)?; |
35 | 41 | } |
36 | 42 |
|
37 | 43 | let git_styles = GitStyles::new(); |
38 | 44 | let ls_styles = LsStyles::from_env(); |
39 | | - let output_mode = vt_config |
40 | | - .map(|cfg| cfg.ui.tool_output_mode) |
41 | | - .unwrap_or(ToolOutputMode::Compact); |
42 | | - let tail_limit = resolve_stdout_tail_limit(vt_config); |
43 | 45 |
|
44 | 46 | if let Some(stdout) = val.get("stdout").and_then(|value| value.as_str()) { |
45 | 47 | render_stream_section( |
@@ -467,21 +469,24 @@ fn resolve_stdout_tail_limit(config: Option<&VTCodeConfig>) -> usize { |
467 | 469 | } |
468 | 470 |
|
469 | 471 | fn tail_lines<'a>(text: &'a str, limit: usize) -> (Vec<&'a str>, usize) { |
| 472 | + if text.is_empty() { |
| 473 | + return (Vec::new(), 0); |
| 474 | + } |
470 | 475 | if limit == 0 { |
471 | 476 | return (Vec::new(), text.lines().count()); |
472 | 477 | } |
473 | 478 |
|
474 | | - let mut lines: Vec<&str> = text.lines().collect(); |
475 | | - let total = lines.len(); |
476 | | - if total == 0 { |
477 | | - return (Vec::new(), 0); |
478 | | - } |
479 | | - if total <= limit { |
480 | | - return (lines, total); |
| 479 | + let mut ring = VecDeque::with_capacity(limit); |
| 480 | + let mut total = 0; |
| 481 | + for line in text.lines() { |
| 482 | + total += 1; |
| 483 | + if ring.len() == limit { |
| 484 | + ring.pop_front(); |
| 485 | + } |
| 486 | + ring.push_back(line); |
481 | 487 | } |
482 | | - let start = total.saturating_sub(limit); |
483 | | - let tail = lines.split_off(start); |
484 | | - (tail, total) |
| 488 | + |
| 489 | + (ring.into_iter().collect(), total) |
485 | 490 | } |
486 | 491 |
|
487 | 492 | fn select_stream_lines<'a>( |
@@ -558,8 +563,12 @@ fn render_stream_section( |
558 | 563 | Ok(()) |
559 | 564 | } |
560 | 565 |
|
561 | | -fn render_curl_result(renderer: &mut AnsiRenderer, val: &Value) -> Result<()> { |
562 | | - const PREVIEW_LINES: usize = 5; |
| 566 | +fn render_curl_result( |
| 567 | + renderer: &mut AnsiRenderer, |
| 568 | + val: &Value, |
| 569 | + mode: ToolOutputMode, |
| 570 | + tail_limit: usize, |
| 571 | +) -> Result<()> { |
563 | 572 | const PREVIEW_LINE_MAX: usize = 120; |
564 | 573 | const NOTICE_MAX: usize = 160; |
565 | 574 |
|
@@ -624,34 +633,29 @@ fn render_curl_result(renderer: &mut AnsiRenderer, val: &Value) -> Result<()> { |
624 | 633 | if let Some(body) = val.get("body").and_then(|value| value.as_str()) |
625 | 634 | && !body.trim().is_empty() |
626 | 635 | { |
627 | | - let mut lines = body.lines(); |
628 | | - let mut preview: Vec<String> = Vec::new(); |
| 636 | + let prefer_full = renderer.prefers_untruncated_output(); |
| 637 | + let (lines, total, truncated) = select_stream_lines(body, mode, tail_limit, prefer_full); |
| 638 | + let tail_len = lines.len(); |
629 | 639 |
|
630 | | - for _ in 0..PREVIEW_LINES { |
631 | | - if let Some(line) = lines.next() { |
632 | | - let trimmed = line.trim_end(); |
633 | | - if trimmed.is_empty() { |
634 | | - continue; |
635 | | - } |
636 | | - preview.push(truncate_text(trimmed, PREVIEW_LINE_MAX)); |
637 | | - } else { |
638 | | - break; |
| 640 | + if tail_len > 0 { |
| 641 | + if truncated { |
| 642 | + renderer.line( |
| 643 | + MessageStyle::Info, |
| 644 | + &format!(" ... showing last {}/{} body lines", tail_len, total), |
| 645 | + )?; |
639 | 646 | } |
640 | | - } |
641 | 647 |
|
642 | | - if !preview.is_empty() { |
643 | 648 | renderer.line( |
644 | 649 | MessageStyle::Tool, |
645 | | - &format!("[curl] body preview ({} lines)", preview.len()), |
| 650 | + &format!("[curl] body tail ({} lines)", tail_len), |
646 | 651 | )?; |
647 | | - for line in preview { |
648 | | - renderer.line(MessageStyle::Output, &format!(" {line}"))?; |
649 | | - } |
650 | 652 |
|
651 | | - if lines.next().is_some() { |
| 653 | + for line in lines { |
| 654 | + let trimmed = line.trim_end(); |
| 655 | + |
652 | 656 | renderer.line( |
653 | | - MessageStyle::Info, |
654 | | - &format!(" … truncated after {PREVIEW_LINES} lines"), |
| 657 | + MessageStyle::Output, |
| 658 | + &format!(" {}", truncate_text(trimmed, PREVIEW_LINE_MAX)), |
655 | 659 | )?; |
656 | 660 | } |
657 | 661 | } |
@@ -689,8 +693,6 @@ impl GitStyles { |
689 | 693 | } |
690 | 694 | } |
691 | 695 |
|
692 | | -use std::collections::HashMap; |
693 | | - |
694 | 696 | struct LsStyles { |
695 | 697 | classes: HashMap<String, Style>, |
696 | 698 | suffixes: Vec<(String, Style)>, |
|
0 commit comments