perf(tui): pre-compute tool output summaries and collapse live view
Pre-compute render caches to avoid re-parsing every frame: - Add output_summary: Option<String> to ExecCell and GenericToolCell - Add is_diff: bool to GenericToolCell (cached after first detection) - Populate caches once in handle_tool_call_complete / orphan path Live mode rendering simplified to one-line summary + expand affordance: - GenericToolCell and ExecCell now show a single muted summary line with "Enter to expand tool output" affordance in Live mode - Transcript mode still emits full output - render_tool_output_summary_line truncates to fit terminal width - Make output_looks_like_diff pub(crate) for pre-computation access Test plan: - cargo test -p deepseek-tui (2379 passed) - config_ui::build_document_reflects_app_state is a pre-existing failure Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -335,6 +335,7 @@ mod tests {
|
||||
duration_ms: None,
|
||||
source: ExecSource::Assistant,
|
||||
interaction: None,
|
||||
output_summary: None,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -355,6 +356,8 @@ mod tests {
|
||||
output: None,
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
+113
-48
@@ -665,6 +665,8 @@ pub struct ExecCell {
|
||||
pub duration_ms: Option<u64>,
|
||||
pub source: ExecSource,
|
||||
pub interaction: Option<String>,
|
||||
/// Cached output summary — avoids re-parsing JSON every frame.
|
||||
pub output_summary: Option<String>,
|
||||
}
|
||||
|
||||
impl ExecCell {
|
||||
@@ -716,12 +718,25 @@ impl ExecCell {
|
||||
|
||||
if self.interaction.is_none() {
|
||||
if let Some(output) = self.output.as_ref() {
|
||||
lines.extend(render_exec_output_mode(
|
||||
output,
|
||||
width,
|
||||
TOOL_OUTPUT_LINE_LIMIT,
|
||||
mode,
|
||||
));
|
||||
if matches!(mode, RenderMode::Live) {
|
||||
let fallback = summarize_tool_output(output);
|
||||
let summary = self
|
||||
.output_summary
|
||||
.as_deref()
|
||||
.unwrap_or(&fallback);
|
||||
lines.push(render_tool_output_summary_line(&summary, width));
|
||||
lines.push(details_affordance_line(
|
||||
"Enter to expand tool output",
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
));
|
||||
} else {
|
||||
lines.extend(render_exec_output_mode(
|
||||
output,
|
||||
width,
|
||||
TOOL_OUTPUT_LINE_LIMIT,
|
||||
mode,
|
||||
));
|
||||
}
|
||||
} else if self.status == ToolStatus::Running && self.source == ExecSource::Assistant {
|
||||
lines.extend(wrap_plain_line(
|
||||
" Ctrl+B opens shell controls.",
|
||||
@@ -1219,6 +1234,11 @@ pub struct GenericToolCell {
|
||||
/// OSC 8-aware terminals — the path renders as a hyperlink when
|
||||
/// `tui.osc8_links` is enabled).
|
||||
pub spillover_path: Option<std::path::PathBuf>,
|
||||
// --- Pre-computed render cache (populated once at cell creation) ---
|
||||
/// Cached output summary — avoids re-parsing JSON every frame.
|
||||
pub output_summary: Option<String>,
|
||||
/// Whether the output looks like a unified diff (cached after first check).
|
||||
pub is_diff: bool,
|
||||
}
|
||||
|
||||
impl GenericToolCell {
|
||||
@@ -1307,10 +1327,8 @@ impl GenericToolCell {
|
||||
}
|
||||
|
||||
if let Some(output) = self.output.as_ref() {
|
||||
// If the output looks like a unified diff (contains hunk headers),
|
||||
// use the full diff renderer with line numbers and colored gutters
|
||||
// instead of the generic output path (#380).
|
||||
if output_looks_like_diff(output) {
|
||||
// Use cached diff detection instead of scanning every frame.
|
||||
if self.is_diff {
|
||||
let diff_summary = diff_render::diff_summary_label(output);
|
||||
lines.push(render_tool_header_with_summary(
|
||||
"Diff",
|
||||
@@ -1321,12 +1339,20 @@ impl GenericToolCell {
|
||||
low_motion,
|
||||
));
|
||||
lines.extend(diff_render::render_diff(output, width));
|
||||
} else if matches!(mode, RenderMode::Live) {
|
||||
// Live mode: one-line summary + expand affordance.
|
||||
let fallback = summarize_tool_output(output);
|
||||
let summary = self
|
||||
.output_summary
|
||||
.as_deref()
|
||||
.unwrap_or(&fallback);
|
||||
lines.push(render_tool_output_summary_line(&summary, width));
|
||||
lines.push(details_affordance_line(
|
||||
"Enter to expand tool output",
|
||||
Style::default().fg(palette::TEXT_MUTED),
|
||||
));
|
||||
} else {
|
||||
// Multi-line outputs (diff stats, file lists, todo snapshots) used
|
||||
// to be crushed into one line by `render_compact_kv` because its
|
||||
// wrapper joined the entire string before wrapping. Route through
|
||||
// `render_tool_output_mode` so each `\n` becomes a real row, with
|
||||
// a `+N more lines` affordance in live mode (#80).
|
||||
// Transcript mode: full output.
|
||||
lines.extend(render_tool_output_mode(
|
||||
output,
|
||||
width,
|
||||
@@ -1335,13 +1361,6 @@ impl GenericToolCell {
|
||||
));
|
||||
}
|
||||
|
||||
// #423: surface the spillover-file path inline so the user
|
||||
// (and the model) can find the elided tail. Only emitted in
|
||||
// live mode — transcript replay already has the full output
|
||||
// verbatim. The path is OSC 8-wrapped when the feature is
|
||||
// enabled so terminals that support hyperlinks make it
|
||||
// Cmd+click-openable; the clipboard / selection path
|
||||
// strips the escape on copy.
|
||||
if matches!(mode, RenderMode::Live)
|
||||
&& let Some(path) = self.spillover_path.as_ref()
|
||||
{
|
||||
@@ -1483,7 +1502,7 @@ fn is_checklist_tool_name(name: &str) -> bool {
|
||||
/// Heuristic: does the output look like a unified diff? Returns true when
|
||||
/// the output contains at least one hunk header (`@@`) or a `diff --git`
|
||||
/// line, which are reliable markers of unified diff content (#380).
|
||||
fn output_looks_like_diff(output: &str) -> bool {
|
||||
pub(crate) fn output_looks_like_diff(output: &str) -> bool {
|
||||
let mut lines = output.lines();
|
||||
// Check first 5 lines for diff markers
|
||||
for _ in 0..5 {
|
||||
@@ -2252,6 +2271,17 @@ fn render_compact_kv(label: &str, value: &str, style: Style, width: u16) -> Vec<
|
||||
render_card_detail_line(Some(label.trim_end_matches(':')), value, style, width)
|
||||
}
|
||||
|
||||
/// Render a single-line tool output summary for the collapsed (Live) view.
|
||||
/// Truncates to fit within `width` columns.
|
||||
fn render_tool_output_summary_line(summary: &str, width: u16) -> Line<'static> {
|
||||
let max_chars = usize::from(width).saturating_sub(2);
|
||||
let text = truncate_text(summary.trim(), max_chars.min(TOOL_TEXT_LIMIT));
|
||||
Line::from(vec![
|
||||
Span::raw(" "),
|
||||
Span::styled(text, Style::default().fg(palette::TEXT_MUTED)),
|
||||
])
|
||||
}
|
||||
|
||||
fn render_tool_output_mode(
|
||||
output: &str,
|
||||
width: u16,
|
||||
@@ -3137,6 +3167,8 @@ mod tests {
|
||||
spillover_path: Some(PathBuf::from(
|
||||
"/Users/dev/.deepseek/tool_outputs/call-abc12.txt",
|
||||
)),
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
};
|
||||
let lines = cell.lines_with_mode(120, true, super::RenderMode::Live);
|
||||
let joined: String = lines
|
||||
@@ -3165,6 +3197,8 @@ mod tests {
|
||||
output: Some("output".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: Some(PathBuf::from("/tmp/spill.txt")),
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
};
|
||||
let lines = cell.lines_with_mode(120, true, super::RenderMode::Transcript);
|
||||
let joined: String = lines
|
||||
@@ -3187,6 +3221,8 @@ mod tests {
|
||||
output: Some("contents".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
};
|
||||
let lines = cell.lines_with_mode(80, true, super::RenderMode::Live);
|
||||
let joined: String = lines
|
||||
@@ -3207,6 +3243,8 @@ mod tests {
|
||||
output: Some("output".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: Some(PathBuf::from(long_path)),
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
};
|
||||
let lines = cell.lines_with_mode(40, true, super::RenderMode::Live);
|
||||
let annotation_line = lines
|
||||
@@ -3282,6 +3320,8 @@ mod tests {
|
||||
),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
};
|
||||
let lines = cell.lines_with_mode(80, true, super::RenderMode::Live);
|
||||
// One header line, no details/args/output expansion.
|
||||
@@ -3315,6 +3355,8 @@ mod tests {
|
||||
output: None,
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
};
|
||||
let lines = cell.lines_with_mode(80, true, super::RenderMode::Live);
|
||||
assert_eq!(lines.len(), 1);
|
||||
@@ -3335,6 +3377,8 @@ mod tests {
|
||||
),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
};
|
||||
let lines = cell.lines_with_mode(80, true, super::RenderMode::Transcript);
|
||||
// Transcript mode emits header + name kv + (no args, output present)
|
||||
@@ -3353,6 +3397,8 @@ mod tests {
|
||||
output: Some("first line\nsecond line\nthird line".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
};
|
||||
let lines = cell.lines_with_mode(80, true, super::RenderMode::Live);
|
||||
assert!(
|
||||
@@ -3607,6 +3653,7 @@ mod tests {
|
||||
duration_ms: None,
|
||||
source: ExecSource::Assistant,
|
||||
interaction: None,
|
||||
output_summary: None,
|
||||
}));
|
||||
|
||||
let animated = cell.lines_with_options(80, TranscriptRenderOptions::default());
|
||||
@@ -3822,6 +3869,7 @@ mod tests {
|
||||
duration_ms: Some(10),
|
||||
source: ExecSource::Assistant,
|
||||
interaction: None,
|
||||
output_summary: None,
|
||||
};
|
||||
let header = &cell.lines_with_motion(80, true)[0];
|
||||
let visible: String = header
|
||||
@@ -3851,6 +3899,7 @@ mod tests {
|
||||
duration_ms: None,
|
||||
source: ExecSource::Assistant,
|
||||
interaction: None,
|
||||
output_summary: None,
|
||||
};
|
||||
|
||||
let header = &cell.lines_with_motion(80, true)[0];
|
||||
@@ -3875,6 +3924,8 @@ mod tests {
|
||||
output: None,
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
};
|
||||
let lines = cell.lines_with_mode(80, true, super::RenderMode::Live);
|
||||
let header_visible: String = lines[0]
|
||||
@@ -3902,6 +3953,8 @@ mod tests {
|
||||
output: None,
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
};
|
||||
let lines = cell.lines_with_mode(80, true, super::RenderMode::Live);
|
||||
let header_visible: String = lines[0]
|
||||
@@ -4145,6 +4198,7 @@ mod tests {
|
||||
duration_ms: Some(42),
|
||||
source: ExecSource::Assistant,
|
||||
interaction: None,
|
||||
output_summary: None,
|
||||
};
|
||||
|
||||
let lines = cell.lines_with_motion(80, true);
|
||||
@@ -4293,10 +4347,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn tool_exec_live_caps_output_transcript_does_not() {
|
||||
// Synthesize an exec output that comfortably exceeds the live cap
|
||||
// (TOOL_OUTPUT_LINE_LIMIT = 6). The live view should hit the cap
|
||||
// and emit a "+N more lines; press v for details" affordance; the
|
||||
// transcript view should emit every wrapped line uncapped.
|
||||
// Live mode shows a one-line summary + "Enter to expand" affordance.
|
||||
// Transcript mode emits the full output uncapped.
|
||||
let total_output_lines = 30usize;
|
||||
let output = (0..total_output_lines)
|
||||
.map(|i| format!("output line {i:02}"))
|
||||
@@ -4311,6 +4363,7 @@ mod tests {
|
||||
duration_ms: Some(120),
|
||||
source: ExecSource::Assistant,
|
||||
interaction: None,
|
||||
output_summary: None,
|
||||
}));
|
||||
|
||||
let live = cell.lines_with_options(
|
||||
@@ -4332,15 +4385,13 @@ mod tests {
|
||||
transcript.len()
|
||||
);
|
||||
assert!(
|
||||
live_text.contains("Alt+V for details"),
|
||||
"live exec output must surface the pager affordance: {live_text}"
|
||||
live_text.contains("Enter to expand tool output"),
|
||||
"live exec output must surface the expand affordance: {live_text}"
|
||||
);
|
||||
assert!(
|
||||
!transcript_text.contains("Alt+V for details"),
|
||||
"transcript exec output must not include the pager affordance"
|
||||
!transcript_text.contains("Enter to expand tool output"),
|
||||
"transcript exec output must not include the expand affordance"
|
||||
);
|
||||
// First line is always emitted on both surfaces.
|
||||
assert!(live_text.contains("output line 00"));
|
||||
assert!(transcript_text.contains("output line 00"));
|
||||
// The middle should only appear in the transcript, since the live
|
||||
// view truncates the head/tail around the cap.
|
||||
@@ -4370,6 +4421,8 @@ mod tests {
|
||||
"Diff this commit against main".to_string(),
|
||||
]),
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
}));
|
||||
let text = lines_text(&cell.lines(80));
|
||||
|
||||
@@ -4395,6 +4448,8 @@ mod tests {
|
||||
output: None,
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
}));
|
||||
let text = lines_text(&cell.lines(80));
|
||||
assert!(text.contains("query: foo"));
|
||||
@@ -4418,6 +4473,8 @@ mod tests {
|
||||
output: Some(diff_stat.to_string()),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
}));
|
||||
|
||||
let transcript_text = lines_text(&cell.transcript_lines(80));
|
||||
@@ -4468,6 +4525,8 @@ mod tests {
|
||||
output: Some(output),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
}));
|
||||
|
||||
let live = cell.lines_with_options(80, TranscriptRenderOptions::default());
|
||||
@@ -4481,17 +4540,15 @@ mod tests {
|
||||
);
|
||||
let live_text = lines_text(&live);
|
||||
assert!(
|
||||
live_text.contains("Alt+V for details"),
|
||||
"live view must show pager affordance: {live_text}"
|
||||
live_text.contains("Enter to expand"),
|
||||
"live view must show expand affordance: {live_text}"
|
||||
);
|
||||
// First line shows up in both; later rows only in transcript.
|
||||
assert!(live_text.contains("row 00"));
|
||||
let transcript_text = lines_text(&transcript);
|
||||
assert!(transcript_text.contains("row 29"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn generic_tool_output_live_keeps_tail_and_omitted_count() {
|
||||
fn generic_tool_output_live_shows_one_line_summary_with_expand_affordance() {
|
||||
let output = (0..24usize)
|
||||
.map(|i| format!("line {i:02}"))
|
||||
.collect::<Vec<_>>()
|
||||
@@ -4503,22 +4560,24 @@ mod tests {
|
||||
output: Some(output),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: Some("24 lines of shell output".to_string()),
|
||||
is_diff: false,
|
||||
}));
|
||||
|
||||
let live_text =
|
||||
lines_text(&cell.lines_with_options(80, TranscriptRenderOptions::default()));
|
||||
|
||||
assert!(live_text.contains("line 00"));
|
||||
assert!(live_text.contains("line 23"));
|
||||
assert!(live_text.contains("lines omitted; Alt+V for details"));
|
||||
// Live mode: one-line summary + expand affordance, NOT full multi-line output.
|
||||
assert!(live_text.contains("Enter to expand tool output"),
|
||||
"live view must show expand affordance: {live_text}");
|
||||
assert!(
|
||||
!live_text.contains("line 12"),
|
||||
"middle plain output should stay omitted in live view: {live_text}"
|
||||
live_text.contains("24 lines of shell output"),
|
||||
"live summary must show the pre-computed output summary: {live_text}"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn tool_output_live_preserves_error_and_path_lines_from_middle() {
|
||||
fn tool_output_live_preserves_summary_with_expand_affordance() {
|
||||
let output = [
|
||||
"start",
|
||||
"still starting",
|
||||
@@ -4538,15 +4597,21 @@ mod tests {
|
||||
output: Some(output),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: Some("Error: failed to read config".to_string()),
|
||||
is_diff: false,
|
||||
}));
|
||||
|
||||
let live_text =
|
||||
lines_text(&cell.lines_with_options(80, TranscriptRenderOptions::default()));
|
||||
|
||||
assert!(live_text.contains("fatal: failed to read /tmp/deepseek/config.toml"));
|
||||
assert!(live_text.contains("https://example.test/build/log"));
|
||||
assert!(live_text.contains("final line"));
|
||||
assert!(live_text.contains("lines omitted; Alt+V for details"));
|
||||
// Live mode: one-line summary + expand affordance.
|
||||
assert!(live_text.contains("Enter to expand tool output"),
|
||||
"live view must show expand affordance: {live_text}");
|
||||
// The pre-computed summary captures the first meaningful content.
|
||||
assert!(
|
||||
live_text.contains("Error:") || live_text.contains("fatal:"),
|
||||
"live summary should capture error text: {live_text}"
|
||||
);
|
||||
}
|
||||
|
||||
// === ErrorEnvelope severity → cell color tests (#66) ===
|
||||
|
||||
@@ -11,7 +11,8 @@ use crate::tui::app::{App, ToolDetailRecord};
|
||||
use crate::tui::history::{
|
||||
DiffPreviewCell, ExecCell, ExecSource, ExploringEntry, GenericToolCell, HistoryCell,
|
||||
McpToolCell, PatchSummaryCell, PlanStep, PlanUpdateCell, ReviewCell, ToolCell, ToolStatus,
|
||||
ViewImageCell, WebSearchCell, summarize_mcp_output, summarize_tool_args, summarize_tool_output,
|
||||
ViewImageCell, WebSearchCell, output_looks_like_diff, summarize_mcp_output,
|
||||
summarize_tool_args, summarize_tool_output,
|
||||
};
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
@@ -109,6 +110,7 @@ pub(super) fn handle_tool_call_started(
|
||||
duration_ms: None,
|
||||
source,
|
||||
interaction: Some(summary.clone()),
|
||||
output_summary: None,
|
||||
})),
|
||||
);
|
||||
return;
|
||||
@@ -140,6 +142,7 @@ pub(super) fn handle_tool_call_started(
|
||||
duration_ms: None,
|
||||
source,
|
||||
interaction: None,
|
||||
output_summary: None,
|
||||
})),
|
||||
);
|
||||
return;
|
||||
@@ -258,6 +261,8 @@ pub(super) fn handle_tool_call_started(
|
||||
output: None,
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})),
|
||||
);
|
||||
}
|
||||
@@ -534,11 +539,15 @@ pub(super) fn handle_tool_call_complete(
|
||||
.and_then(serde_json::Value::as_u64);
|
||||
if status != ToolStatus::Running && exec.interaction.is_none() {
|
||||
exec.output = Some(tool_result.content.clone());
|
||||
exec.output_summary =
|
||||
Some(super::history::summarize_tool_output(&tool_result.content));
|
||||
}
|
||||
} else if let Err(err) = result.as_ref()
|
||||
&& exec.interaction.is_none()
|
||||
{
|
||||
exec.output = Some(err.to_string());
|
||||
exec.output_summary =
|
||||
Some(super::history::summarize_tool_output(&err.to_string()));
|
||||
}
|
||||
app.mark_history_updated();
|
||||
}
|
||||
@@ -614,10 +623,17 @@ pub(super) fn handle_tool_call_complete(
|
||||
generic.status = status;
|
||||
match result.as_ref() {
|
||||
Ok(tool_result) => {
|
||||
generic.output = Some(summarize_tool_output(&tool_result.content));
|
||||
generic.output = Some(tool_result.content.clone());
|
||||
generic.output_summary =
|
||||
Some(summarize_tool_output(&tool_result.content));
|
||||
generic.is_diff =
|
||||
output_looks_like_diff(&tool_result.content);
|
||||
}
|
||||
Err(err) => {
|
||||
generic.output = Some(err.to_string());
|
||||
generic.output_summary =
|
||||
Some(summarize_tool_output(&err.to_string()));
|
||||
generic.is_diff = false;
|
||||
}
|
||||
}
|
||||
app.mark_history_updated();
|
||||
@@ -699,6 +715,12 @@ fn push_orphan_tool_completion(
|
||||
.and_then(|m| m.get("spillover_path"))
|
||||
.and_then(serde_json::Value::as_str)
|
||||
.map(std::path::PathBuf::from);
|
||||
let output_summary = output
|
||||
.as_deref()
|
||||
.map(|o| summarize_tool_output(o));
|
||||
let is_diff = output
|
||||
.as_deref()
|
||||
.is_some_and(|o| output_looks_like_diff(o));
|
||||
app.add_message(HistoryCell::Tool(ToolCell::Generic(GenericToolCell {
|
||||
name: name.to_string(),
|
||||
status,
|
||||
@@ -706,6 +728,8 @@ fn push_orphan_tool_completion(
|
||||
output,
|
||||
prompts: None,
|
||||
spillover_path,
|
||||
output_summary,
|
||||
is_diff,
|
||||
})));
|
||||
let cell_index = app.history.len().saturating_sub(1);
|
||||
app.tool_details_by_cell.insert(
|
||||
|
||||
@@ -564,6 +564,7 @@ mod tests {
|
||||
duration_ms: None,
|
||||
source: ExecSource::Assistant,
|
||||
interaction: None,
|
||||
output_summary: None,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -246,6 +246,8 @@ fn selection_to_text_copies_rendered_transcript_block() {
|
||||
output: Some("tool output line".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})),
|
||||
HistoryCell::Assistant {
|
||||
content: "copy assistant".to_string(),
|
||||
@@ -1195,6 +1197,7 @@ fn active_tool_status_label_summarizes_live_tool_group() {
|
||||
duration_ms: None,
|
||||
source: ExecSource::Assistant,
|
||||
interaction: None,
|
||||
output_summary: None,
|
||||
})),
|
||||
);
|
||||
active.push_tool(
|
||||
@@ -1206,6 +1209,8 @@ fn active_tool_status_label_summarizes_live_tool_group() {
|
||||
output: Some("done".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})),
|
||||
);
|
||||
app.active_cell = Some(active);
|
||||
@@ -1232,6 +1237,8 @@ fn active_tool_status_label_counts_foreground_rlm_work() {
|
||||
output: None,
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})),
|
||||
);
|
||||
app.active_cell = Some(active);
|
||||
@@ -2495,6 +2502,8 @@ fn jump_to_adjacent_tool_cell_finds_next_and_previous() {
|
||||
output: Some("done".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})),
|
||||
HistoryCell::Assistant {
|
||||
content: "ok".to_string(),
|
||||
@@ -2507,6 +2516,8 @@ fn jump_to_adjacent_tool_cell_finds_next_and_previous() {
|
||||
output: Some("...".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})),
|
||||
];
|
||||
app.mark_history_updated();
|
||||
@@ -2563,6 +2574,8 @@ fn detail_target_prefers_visible_tool_card() {
|
||||
output: Some("done".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})),
|
||||
HistoryCell::Assistant {
|
||||
content: "ok".to_string(),
|
||||
@@ -2575,6 +2588,8 @@ fn detail_target_prefers_visible_tool_card() {
|
||||
output: Some("...".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})),
|
||||
];
|
||||
app.tool_details_by_cell.insert(
|
||||
@@ -2666,6 +2681,8 @@ fn spillover_pager_section_returns_none_when_no_spillover() {
|
||||
output: Some("hi".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
}))];
|
||||
app.resync_history_revisions();
|
||||
assert!(spillover_pager_section(&app, 0).is_none());
|
||||
@@ -2687,6 +2704,8 @@ fn spillover_pager_section_loads_file_when_present() {
|
||||
output: Some("(truncated head)".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: Some(path.clone()),
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
}))];
|
||||
app.resync_history_revisions();
|
||||
|
||||
@@ -2710,6 +2729,8 @@ fn spillover_pager_section_returns_notice_when_file_missing() {
|
||||
output: Some("(truncated head)".to_string()),
|
||||
prompts: None,
|
||||
spillover_path: Some(bogus),
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
}))];
|
||||
app.resync_history_revisions();
|
||||
|
||||
@@ -2733,6 +2754,7 @@ fn terminal_pause_has_live_owner_only_for_running_exec_cells() {
|
||||
duration_ms: None,
|
||||
source: ExecSource::Assistant,
|
||||
interaction: Some("interactive".to_string()),
|
||||
output_summary: None,
|
||||
})),
|
||||
);
|
||||
app.active_cell = Some(active);
|
||||
@@ -2748,6 +2770,8 @@ fn terminal_pause_has_live_owner_only_for_running_exec_cells() {
|
||||
output: None,
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})),
|
||||
);
|
||||
app.active_cell = Some(active);
|
||||
@@ -2771,6 +2795,8 @@ fn active_rlm_task_entries_surface_foreground_rlm_work() {
|
||||
output: None,
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})),
|
||||
);
|
||||
app.active_cell = Some(active);
|
||||
@@ -4348,6 +4374,8 @@ fn checklist_write_renders_dedicated_card() {
|
||||
),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
};
|
||||
let lines = cell.lines_with_mode(80, true, crate::tui::history::RenderMode::Live);
|
||||
let text: Vec<String> = lines
|
||||
|
||||
@@ -2687,6 +2687,8 @@ mod tests {
|
||||
output: Some("hello world ".repeat(420)),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
}));
|
||||
for width in [40u16, 80, 111, 165] {
|
||||
let lines = cell.lines(width);
|
||||
@@ -2731,6 +2733,8 @@ mod tests {
|
||||
output: Some(output),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
})));
|
||||
|
||||
let height: u16 = 30;
|
||||
@@ -3109,6 +3113,8 @@ mod tests {
|
||||
output: Some(format!("found 12 matches in cell-{i}")),
|
||||
prompts: None,
|
||||
spillover_path: None,
|
||||
output_summary: None,
|
||||
is_diff: false,
|
||||
}))
|
||||
} else if i % 2 == 0 {
|
||||
HistoryCell::User {
|
||||
|
||||
Reference in New Issue
Block a user