Synced from monorepo
Synced from monorepo Changes: - grok-shell: send an expired external-provider credential to the sign-in flow, not a 401 loop - pager: clickable ▲ jumps to the top of the response being read - grok-shell: keep a large task log from making the completion message too long - Plan viewer scrollbar: widen grab zone to the border column; fix striped thumb in Terminal.app - pager: poll the tmux probe teardown grace instead of sleeping it - security: vendor-compat MCP kill switch is now actually enforced when reported as on - grok-shell: restore session eviction when a leader client disconnects - Bump rust-toolchain to 1.93.0 - workspace: lexical-normalize permission path patterns before glob matching - pager: reject garbage Enter in the /resume picker - pager: show Mermaid affordances in plan mode preview - pager: drop manage-account link from /session-info - workspace: auto-approve read-only git queries; defer write floor to auto classifier - Add free-form pattern editor to the "Always allow" command prompt - grok-shell: fix /btw caching - pager: Tab walks answers in the ask_user_question card - External-provider auth refresh: single 7s attempt instead of 3×5s - pager: don't resurrect finished background tasks as Running when completion arrives first - pager: report tmux truecolor clamping in Doctor - Fix plan viewer scrollbar click+drag hijacked by comment gutter - pager/shell: stop double Recap after the same last turn - sampler: preserve x-should-retry through stream collection - pager: clear plan-mode indicator immediately when the user approves a plan - pager: tmux does not re-read its config on reattach Source-Revision: 64c4de99cc822b25ce9c54ab5a4f372093d0885d
This commit is contained in:
parent
a422116582
commit
780d1388ff
323 changed files with 12258 additions and 7226 deletions
|
|
@ -92,25 +92,61 @@ pub fn truncate_str(s: &str, max_bytes: usize) -> &str {
|
|||
&s[..end]
|
||||
}
|
||||
|
||||
/// Text on hand, and the size of the output it came from. The two differ when
|
||||
/// the caller holds only part of a larger output and the reader still needs
|
||||
/// the real size.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||
pub struct PartialOutput<'a> {
|
||||
text: &'a str,
|
||||
total_bytes: usize,
|
||||
}
|
||||
|
||||
impl<'a> PartialOutput<'a> {
|
||||
pub fn whole(text: &'a str) -> Self {
|
||||
Self {
|
||||
text,
|
||||
total_bytes: text.len(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Part of an output of `total_bytes`.
|
||||
pub fn part_of(text: &'a str, total_bytes: usize) -> Self {
|
||||
Self {
|
||||
text,
|
||||
total_bytes: total_bytes.max(text.len()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn text(&self) -> &'a str {
|
||||
self.text
|
||||
}
|
||||
|
||||
pub fn total_bytes(&self) -> usize {
|
||||
self.total_bytes
|
||||
}
|
||||
}
|
||||
|
||||
/// Truncate output to a UTF-8-safe preview plus a model-visible footer.
|
||||
///
|
||||
/// The cap decides whether truncation happens. When triggered, the returned
|
||||
/// value contains the first `preview_bytes` bytes snapped to a char boundary
|
||||
/// followed by `[Output truncated - <N> bytes total...]`.
|
||||
/// followed by `[Output truncated - <N> bytes total...]`, where `N` is the
|
||||
/// size of the whole output, not of the part on hand.
|
||||
pub fn truncate_with_preview(
|
||||
output: &str,
|
||||
output: PartialOutput<'_>,
|
||||
max_bytes: usize,
|
||||
preview_bytes: usize,
|
||||
footer_hint: Option<&str>,
|
||||
) -> (String, bool) {
|
||||
if output.len() <= max_bytes {
|
||||
return (output.to_string(), false);
|
||||
let PartialOutput { text, total_bytes } = output;
|
||||
if text.len() <= max_bytes {
|
||||
return (text.to_string(), false);
|
||||
}
|
||||
|
||||
let preview = truncate_str(output, preview_bytes.min(output.len()));
|
||||
let preview = truncate_str(text, preview_bytes.min(text.len()));
|
||||
let footer = match footer_hint {
|
||||
Some(hint) => format!("[Output truncated - {} bytes total. {hint}]", output.len()),
|
||||
None => format!("[Output truncated - {} bytes total]", output.len()),
|
||||
Some(hint) => format!("[Output truncated - {total_bytes} bytes total. {hint}]"),
|
||||
None => format!("[Output truncated - {total_bytes} bytes total]"),
|
||||
};
|
||||
(format!("{preview}\n\n{footer}"), true)
|
||||
}
|
||||
|
|
@ -226,6 +262,9 @@ pub fn soft_wrap_lines(text: &str, wrap_width: usize) -> String {
|
|||
result
|
||||
}
|
||||
|
||||
/// Separator between the retained head and tail of a truncated output.
|
||||
pub(crate) const FRONT_BACK_TRUNCATION_MARKER: &str = "\n\n... (output truncated) ...\n\n";
|
||||
|
||||
/// Truncate a string keeping the first half and last half of the character
|
||||
/// budget, inserting a separator in the middle.
|
||||
///
|
||||
|
|
@ -252,7 +291,7 @@ pub fn truncate_front_and_back(s: &str, max_chars: usize) -> (String, bool) {
|
|||
.unwrap_or(0)
|
||||
}
|
||||
};
|
||||
let ellipsis = "\n\n... (output truncated) ...\n\n";
|
||||
let ellipsis = FRONT_BACK_TRUNCATION_MARKER;
|
||||
let mut result = String::with_capacity(front_end + ellipsis.len() + (s.len() - back_start));
|
||||
result.push_str(&s[..front_end]);
|
||||
result.push_str(ellipsis);
|
||||
|
|
@ -505,7 +544,7 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn truncate_with_preview_short_output_unchanged() {
|
||||
let (result, truncated) = truncate_with_preview("hello", 10, 5, None);
|
||||
let (result, truncated) = truncate_with_preview(PartialOutput::whole("hello"), 10, 5, None);
|
||||
assert_eq!(result, "hello");
|
||||
assert!(!truncated);
|
||||
}
|
||||
|
|
@ -513,7 +552,8 @@ mod tests {
|
|||
#[test]
|
||||
fn truncate_with_preview_caps_large_output() {
|
||||
let output = "x".repeat(5_000_000);
|
||||
let (result, truncated) = truncate_with_preview(&output, 4_000, 2_000, None);
|
||||
let (result, truncated) =
|
||||
truncate_with_preview(PartialOutput::whole(&output), 4_000, 2_000, None);
|
||||
|
||||
assert!(truncated);
|
||||
assert!(result.len() < 2_200, "result was {} bytes", result.len());
|
||||
|
|
@ -524,7 +564,8 @@ mod tests {
|
|||
#[test]
|
||||
fn truncate_with_preview_utf8_boundary() {
|
||||
let output = "😀".repeat(1_500);
|
||||
let (result, truncated) = truncate_with_preview(&output, 4_000, 2_001, None);
|
||||
let (result, truncated) =
|
||||
truncate_with_preview(PartialOutput::whole(&output), 4_000, 2_001, None);
|
||||
|
||||
assert!(truncated);
|
||||
assert!(result.starts_with(&"😀".repeat(500)));
|
||||
|
|
@ -535,7 +576,7 @@ mod tests {
|
|||
fn truncate_with_preview_with_footer_hint() {
|
||||
let output = "x".repeat(10_000);
|
||||
let (result, truncated) = truncate_with_preview(
|
||||
&output,
|
||||
PartialOutput::whole(&output),
|
||||
4_000,
|
||||
2_000,
|
||||
Some("Use read_file for full content"),
|
||||
|
|
@ -545,10 +586,22 @@ mod tests {
|
|||
assert!(result.contains("Use read_file for full content"));
|
||||
}
|
||||
|
||||
/// A caller holding part of a larger output states the real size.
|
||||
#[test]
|
||||
fn truncate_with_preview_reports_the_size_it_is_given() {
|
||||
let held = "x".repeat(10_000);
|
||||
let (result, truncated) =
|
||||
truncate_with_preview(PartialOutput::part_of(&held, 5_000_000), 4_000, 2_000, None);
|
||||
|
||||
assert!(truncated);
|
||||
assert!(result.contains("5000000 bytes total"), "{result}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncate_with_preview_without_footer_hint() {
|
||||
let output = "x".repeat(10_000);
|
||||
let (result, truncated) = truncate_with_preview(&output, 4_000, 2_000, None);
|
||||
let (result, truncated) =
|
||||
truncate_with_preview(PartialOutput::whole(&output), 4_000, 2_000, None);
|
||||
|
||||
assert!(truncated);
|
||||
assert!(result.contains("[Output truncated - 10000 bytes total]"));
|
||||
|
|
|
|||
Loading…
Reference in a new issue