Show hidden output line counts in compact terminal activity (#48761)

## What changed

Replace generic output disclosure labels with counts such as `+ 5 lines (ctrl+t to expand)` for compact command activity, both live and in history. Count retained output lines hidden or clipped by the preview, excluding output discarded by storage limits.

Use the configured `open_transcript` shortcut and omit the hint when it does not fit or the action is unbound. Invalidate cached layouts when keybindings change. Keep generic `Show details` labels for other hidden details and keep disclosure controls outside copyable and searchable text.

## Testing

Add regression tests for live/history consistency, remapped and disabled shortcuts, and exclusion of hints from source text. Add snapshot coverage for fully visible output, hidden and clipped lines, command-only details, and storage-truncated output.

GitOrigin-RevId: c468be9b88b69dc3ca1abdb6b7be023244d32a57
This commit is contained in:
Felipe Coury
2026-09-27 18:09:34 +00:00
committed by copyberry
parent 71b38795d8
commit e75b6b1e0c
12 changed files with 263 additions and 34 deletions
@@ -414,7 +414,7 @@ fn activity_focus_keeps_controls_without_passive_hints() {
cell.compact_hyperlink_lines(width)
},
auxiliary: Vec::new(),
has_hidden_details: true,
disclosure: cell.activity_disclosure(width),
})
});
render(view, &[])
+28 -9
View File
@@ -3,6 +3,9 @@
//! Mutable cells refresh each frame, then share that frame's layout across measurement and paint.
//! Stable cells also invalidate when animation ticks, syntax themes, or terminal colors change.
use crate::history_cell::ActivityDisclosure;
use crate::key_hint::ShortcutHint;
use crate::keymap::KeymapContext;
use std::sync::Weak;
use super::*;
@@ -14,7 +17,7 @@ const MAX_CACHED_TEXT_BYTES: usize = 8 * 1024 * 1024;
pub(crate) struct ActivityTranscriptLines {
pub(crate) activity: Vec<HyperlinkLine>,
pub(crate) auxiliary: Vec<HyperlinkLine>,
pub(crate) has_hidden_details: bool,
pub(crate) disclosure: Option<ActivityDisclosure>,
}
/// Keep disclosure controls outside selectable source without rendering hidden details.
@@ -22,8 +25,9 @@ pub(super) fn activity_layout(
mut lines: ActivityTranscriptLines,
width: u16,
expanded: bool,
shortcut: Option<ShortcutHint>,
) -> TextLayout {
let has_details = !lines.activity.is_empty() && (expanded || lines.has_hidden_details);
let has_details = !lines.activity.is_empty() && (expanded || lines.disclosure.is_some());
let source_offset = (!lines.auxiliary.is_empty())
.then(|| TextLayout::new(lines.activity.clone(), width).text().len());
lines.activity.extend(lines.auxiliary);
@@ -31,12 +35,22 @@ pub(super) fn activity_layout(
if !has_details {
return layout;
}
let label = if expanded {
"− Show less"
} else {
"+ Show details"
}
.to_owned();
let label = match (expanded, lines.disclosure) {
(true, _) => "− Show less".to_owned(),
(false, Some(ActivityDisclosure::OutputLines(count))) => {
let noun = if count == 1 { "line" } else { "lines" };
let mut label = format!("+ {count} {noun}");
if let Some(shortcut) = shortcut {
let hint = format!(" ({} to expand)", shortcut.display_label());
let indent = usize::from(width / 4).min(/*other*/ 4);
if Line::from(format!("{label}{hint}")).width() + indent <= usize::from(width) {
label.push_str(&hint);
}
}
label
}
(false, Some(ActivityDisclosure::Generic) | None) => "+ Show details".to_owned(),
};
match source_offset {
Some(offset) => layout.with_disclosure_control_at(label, offset),
None => layout.with_disclosure_control(label),
@@ -141,6 +155,10 @@ impl TranscriptView {
expanded,
disclosure,
};
let shortcut = self
.disclosure
.keymap
.primary_hint(KeymapContext::Global, "open_transcript");
Some(self.cache.get(cell, width, presentation, || {
if disclosure {
activity_layout(
@@ -151,10 +169,11 @@ impl TranscriptView {
cell.compact_hyperlink_lines(width)
},
auxiliary: Vec::new(),
has_hidden_details: cell.has_hidden_activity_details(width),
disclosure: cell.activity_disclosure(width),
},
width,
expanded,
shortcut,
)
} else {
TextLayout::new(
@@ -111,6 +111,8 @@ impl TranscriptView {
}
self.search.editor.set_keymap_bindings(keymap);
self.disclosure.keymap = keymap.clone();
self.cache.clear();
self.live_key = None;
}
pub(crate) fn is_search_active(&self) -> bool {