feat: update splash (#3631)

- Update splash styling.
- Add center truncation for long paths.
  (Uses new `center_truncate_path` utility.)
- Update the suggested commands.


## New splash
<img width="560" height="326" alt="image"
src="https://github.com/user-attachments/assets/b80d7075-f376-4019-a464-b96a78b0676d"
/>

## Example with truncation:
<img width="524" height="317" alt="image"
src="https://github.com/user-attachments/assets/b023c5cc-0bf0-4d21-9b98-bfea85546eda"
/>
This commit is contained in:
ae
2025-09-15 06:44:40 -07:00
committed by GitHub
parent fdf4a68646
commit 9baa5c33da
6 changed files with 379 additions and 64 deletions

View File

@@ -1,4 +1,6 @@
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthChar;
use unicode_width::UnicodeWidthStr;
/// Truncate a tool result to fit within the given height and width. If the text is valid JSON, we format it in a compact way before truncating.
/// This is a best-effort approach that may not work perfectly for text where 1 grapheme is rendered as multiple terminal cells.
@@ -100,6 +102,219 @@ pub(crate) fn truncate_text(text: &str, max_graphemes: usize) -> String {
}
}
/// Truncate a path-like string to the given display width, keeping leading and trailing segments
/// where possible and inserting a single Unicode ellipsis between them. If an individual segment
/// cannot fit, it is front-truncated with an ellipsis.
pub(crate) fn center_truncate_path(path: &str, max_width: usize) -> String {
if max_width == 0 {
return String::new();
}
if UnicodeWidthStr::width(path) <= max_width {
return path.to_string();
}
let sep = std::path::MAIN_SEPARATOR;
let has_leading_sep = path.starts_with(sep);
let has_trailing_sep = path.ends_with(sep);
let mut raw_segments: Vec<&str> = path.split(sep).collect();
if has_leading_sep && !raw_segments.is_empty() && raw_segments[0].is_empty() {
raw_segments.remove(0);
}
if has_trailing_sep
&& !raw_segments.is_empty()
&& raw_segments.last().is_some_and(|last| last.is_empty())
{
raw_segments.pop();
}
if raw_segments.is_empty() {
if has_leading_sep {
let root = sep.to_string();
if UnicodeWidthStr::width(root.as_str()) <= max_width {
return root;
}
}
return "".to_string();
}
struct Segment<'a> {
original: &'a str,
text: String,
truncatable: bool,
is_suffix: bool,
}
let assemble = |leading: bool, segments: &[Segment<'_>]| -> String {
let mut result = String::new();
if leading {
result.push(sep);
}
for segment in segments {
if !result.is_empty() && !result.ends_with(sep) {
result.push(sep);
}
result.push_str(segment.text.as_str());
}
result
};
let front_truncate = |original: &str, allowed_width: usize| -> String {
if allowed_width == 0 {
return String::new();
}
if UnicodeWidthStr::width(original) <= allowed_width {
return original.to_string();
}
if allowed_width == 1 {
return "".to_string();
}
let mut kept: Vec<char> = Vec::new();
let mut used_width = 1; // reserve space for leading ellipsis
for ch in original.chars().rev() {
let ch_width = UnicodeWidthChar::width(ch).unwrap_or(0);
if used_width + ch_width > allowed_width {
break;
}
used_width += ch_width;
kept.push(ch);
}
kept.reverse();
let mut truncated = String::from("");
for ch in kept {
truncated.push(ch);
}
truncated
};
let mut combos: Vec<(usize, usize)> = Vec::new();
let segment_count = raw_segments.len();
for left in 1..=segment_count {
let min_right = if left == segment_count { 0 } else { 1 };
for right in min_right..=(segment_count - left) {
combos.push((left, right));
}
}
let desired_suffix = if segment_count > 1 {
std::cmp::min(2, segment_count - 1)
} else {
0
};
let mut prioritized: Vec<(usize, usize)> = Vec::new();
let mut fallback: Vec<(usize, usize)> = Vec::new();
for combo in combos {
if combo.1 >= desired_suffix {
prioritized.push(combo);
} else {
fallback.push(combo);
}
}
let sort_combos = |items: &mut Vec<(usize, usize)>| {
items.sort_by(|(left_a, right_a), (left_b, right_b)| {
left_b
.cmp(left_a)
.then_with(|| right_b.cmp(right_a))
.then_with(|| (left_b + right_b).cmp(&(left_a + right_a)))
});
};
sort_combos(&mut prioritized);
sort_combos(&mut fallback);
let fit_segments =
|segments: &mut Vec<Segment<'_>>, allow_front_truncate: bool| -> Option<String> {
loop {
let candidate = assemble(has_leading_sep, segments);
let width = UnicodeWidthStr::width(candidate.as_str());
if width <= max_width {
return Some(candidate);
}
if !allow_front_truncate {
return None;
}
let mut indices: Vec<usize> = Vec::new();
for (idx, seg) in segments.iter().enumerate().rev() {
if seg.truncatable && seg.is_suffix {
indices.push(idx);
}
}
for (idx, seg) in segments.iter().enumerate().rev() {
if seg.truncatable && !seg.is_suffix {
indices.push(idx);
}
}
if indices.is_empty() {
return None;
}
let mut changed = false;
for idx in indices {
let original_width = UnicodeWidthStr::width(segments[idx].original);
if original_width <= max_width && segment_count > 2 {
continue;
}
let seg_width = UnicodeWidthStr::width(segments[idx].text.as_str());
let other_width = width.saturating_sub(seg_width);
let allowed_width = max_width.saturating_sub(other_width).max(1);
let new_text = front_truncate(segments[idx].original, allowed_width);
if new_text != segments[idx].text {
segments[idx].text = new_text;
changed = true;
break;
}
}
if !changed {
return None;
}
}
};
for (left_count, right_count) in prioritized.into_iter().chain(fallback.into_iter()) {
let mut segments: Vec<Segment<'_>> = raw_segments[..left_count]
.iter()
.map(|seg| Segment {
original: seg,
text: (*seg).to_string(),
truncatable: true,
is_suffix: false,
})
.collect();
let need_ellipsis = left_count + right_count < segment_count;
if need_ellipsis {
segments.push(Segment {
original: "",
text: "".to_string(),
truncatable: false,
is_suffix: false,
});
}
if right_count > 0 {
segments.extend(
raw_segments[segment_count - right_count..]
.iter()
.map(|seg| Segment {
original: seg,
text: (*seg).to_string(),
truncatable: true,
is_suffix: true,
}),
);
}
let allow_front_truncate = need_ellipsis || segment_count <= 2;
if let Some(candidate) = fit_segments(&mut segments, allow_front_truncate) {
return candidate;
}
}
front_truncate(path, max_width)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -203,6 +418,49 @@ mod tests {
);
}
#[test]
fn test_center_truncate_doesnt_truncate_short_path() {
let sep = std::path::MAIN_SEPARATOR;
let path = format!("{sep}Users{sep}codex{sep}Public");
let truncated = center_truncate_path(&path, 40);
assert_eq!(truncated, path);
}
#[test]
fn test_center_truncate_truncates_long_path() {
let sep = std::path::MAIN_SEPARATOR;
let path = format!("~{sep}hello{sep}the{sep}fox{sep}is{sep}very{sep}fast");
let truncated = center_truncate_path(&path, 24);
assert_eq!(
truncated,
format!("~{sep}hello{sep}the{sep}{sep}very{sep}fast")
);
}
#[test]
fn test_center_truncate_truncates_long_windows_path() {
let sep = std::path::MAIN_SEPARATOR;
let path = format!(
"C:{sep}Users{sep}codex{sep}Projects{sep}super{sep}long{sep}windows{sep}path{sep}file.txt"
);
let truncated = center_truncate_path(&path, 36);
let expected = format!("C:{sep}Users{sep}codex{sep}{sep}path{sep}file.txt");
assert_eq!(truncated, expected);
}
#[test]
fn test_center_truncate_handles_long_segment() {
let sep = std::path::MAIN_SEPARATOR;
let path = format!("~{sep}supercalifragilisticexpialidocious");
let truncated = center_truncate_path(&path, 18);
assert_eq!(truncated, format!("~{sep}…cexpialidocious"));
}
#[test]
fn test_format_json_compact_array() {
let json = r#"[ 1, 2, { "key": "value" }, "string" ]"#;