Skip to content

Commit 9af7b8a

Browse files
committed
fix(tui): make tool calls and subagent display responsive to terminal resize
Previously, tool call displays and subagent task displays used hardcoded width values (60, 70 chars) for text truncation and wrapping. This meant that when the terminal was resized, these elements would not adapt to the new width, potentially causing text to be cut off or not properly wrapped. Changes: - Add width parameter to render_tool_call() and render_subagent() functions - Calculate content_width and line_width dynamically based on terminal width - Apply proper text wrapping using wrap_text() for error messages and arguments - Update all call sites in generate_message_lines() to pass width parameter Now when the terminal is resized, all text content (messages, tool calls, subagent tasks) automatically re-wraps to fit the new terminal width.
1 parent 052c5e0 commit 9af7b8a

1 file changed

Lines changed: 87 additions & 40 deletions

File tree

cortex-tui/src/views/minimal_session.rs

Lines changed: 87 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -289,10 +289,14 @@ impl<'a> MinimalSessionView<'a> {
289289
}
290290

291291
/// Renders a single tool call with status indicator
292-
fn render_tool_call(&self, call: &ToolCallDisplay) -> Vec<Line<'static>> {
292+
fn render_tool_call(&self, call: &ToolCallDisplay, width: u16) -> Vec<Line<'static>> {
293293
use crate::ui::consts::TOOL_SPINNER_FRAMES;
294294
let mut lines = Vec::new();
295295

296+
// Calculate available width for content (accounting for indentation)
297+
let content_width = (width as usize).saturating_sub(6); // 6 chars for prefix/indent
298+
let line_width = (width as usize).saturating_sub(8); // 8 chars for nested content
299+
296300
// Status indicator with color - animated spinner for Running status
297301
let (dot, dot_color) = match call.status {
298302
ToolStatus::Pending => ("○".to_string(), self.colors.warning),
@@ -305,9 +309,9 @@ impl<'a> MinimalSessionView<'a> {
305309
ToolStatus::Failed => ("●".to_string(), self.colors.error),
306310
};
307311

308-
// Line 1: ◐ ToolName summary_args (truncate summary to avoid overflow)
312+
// Line 1: ◐ ToolName summary_args (truncate summary to fit terminal width)
309313
let summary = crate::views::tool_call::format_tool_summary(&call.name, &call.arguments);
310-
let max_summary = 60_usize.saturating_sub(call.name.len());
314+
let max_summary = content_width.saturating_sub(call.name.len() + 2);
311315
let summary_truncated = if summary.len() > max_summary {
312316
format!(
313317
"{}...",
@@ -335,9 +339,15 @@ impl<'a> MinimalSessionView<'a> {
335339
// Live output lines (for Running status with output)
336340
if call.status == ToolStatus::Running && !call.live_output.is_empty() {
337341
for output_line in &call.live_output {
338-
// Truncate long lines to fit display
339-
let truncated = if output_line.len() > 70 {
340-
format!("{}...", &output_line.chars().take(67).collect::<String>())
342+
// Truncate long lines to fit terminal width
343+
let truncated = if output_line.len() > line_width {
344+
format!(
345+
"{}...",
346+
&output_line
347+
.chars()
348+
.take(line_width.saturating_sub(3))
349+
.collect::<String>()
350+
)
341351
} else {
342352
output_line.clone()
343353
};
@@ -348,17 +358,21 @@ impl<'a> MinimalSessionView<'a> {
348358
}
349359
}
350360

351-
// Result summary line (if completed/failed) - truncate to avoid overflow
361+
// Result summary line (if completed/failed) - truncate to fit terminal width
352362
if let Some(ref result) = call.result {
353363
let result_color = if result.success {
354364
self.colors.text_dim
355365
} else {
356366
self.colors.error
357367
};
358-
let summary_truncated = if result.summary.len() > 70 {
368+
let summary_truncated = if result.summary.len() > line_width {
359369
format!(
360370
"{}...",
361-
&result.summary.chars().take(67).collect::<String>()
371+
&result
372+
.summary
373+
.chars()
374+
.take(line_width.saturating_sub(3))
375+
.collect::<String>()
362376
)
363377
} else {
364378
result.summary.clone()
@@ -368,13 +382,21 @@ impl<'a> MinimalSessionView<'a> {
368382
Span::styled(summary_truncated, Style::default().fg(result_color)),
369383
]));
370384

371-
// If error and not collapsed, show full error
385+
// If error and not collapsed, show full error with wrapping
372386
if !result.success && !call.collapsed {
373387
for err_line in result.output.lines().take(5) {
374-
lines.push(Line::from(vec![
375-
Span::raw(" "),
376-
Span::styled(err_line.to_string(), Style::default().fg(self.colors.error)),
377-
]));
388+
// Wrap long error lines
389+
let wrapped = Self::wrap_text(err_line, line_width.saturating_sub(4));
390+
for (i, wrapped_line) in wrapped.iter().enumerate() {
391+
let prefix = if i == 0 { " " } else { " " };
392+
lines.push(Line::from(vec![
393+
Span::raw(prefix.to_string()),
394+
Span::styled(
395+
wrapped_line.clone(),
396+
Style::default().fg(self.colors.error),
397+
),
398+
]));
399+
}
378400
}
379401
}
380402
}
@@ -388,10 +410,18 @@ impl<'a> MinimalSessionView<'a> {
388410
)));
389411
if let Ok(args_str) = serde_json::to_string_pretty(&call.arguments) {
390412
for arg_line in args_str.lines().take(10) {
391-
lines.push(Line::from(vec![
392-
Span::raw(" "),
393-
Span::styled(arg_line.to_string(), Style::default().fg(self.colors.text)),
394-
]));
413+
// Wrap long argument lines
414+
let wrapped = Self::wrap_text(arg_line, line_width.saturating_sub(4));
415+
for (i, wrapped_line) in wrapped.iter().enumerate() {
416+
let prefix = if i == 0 { " " } else { " " };
417+
lines.push(Line::from(vec![
418+
Span::raw(prefix.to_string()),
419+
Span::styled(
420+
wrapped_line.clone(),
421+
Style::default().fg(self.colors.text),
422+
),
423+
]));
424+
}
395425
}
396426
}
397427
}
@@ -409,10 +439,14 @@ impl<'a> MinimalSessionView<'a> {
409439
/// [in_progress] task2
410440
/// [completed] task3
411441
/// ```
412-
fn render_subagent(&self, task: &SubagentTaskDisplay) -> Vec<Line<'static>> {
442+
fn render_subagent(&self, task: &SubagentTaskDisplay, width: u16) -> Vec<Line<'static>> {
413443
use crate::app::SubagentTodoStatus;
414444
let mut lines = Vec::new();
415445

446+
// Calculate available width for content (accounting for indentation)
447+
let content_width = (width as usize).saturating_sub(6); // 6 chars for prefix/indent
448+
let line_width = (width as usize).saturating_sub(8); // 8 chars for nested content
449+
416450
// Status indicator with color
417451
let (indicator, indicator_color) = match &task.status {
418452
SubagentDisplayStatus::Starting
@@ -441,18 +475,19 @@ impl<'a> MinimalSessionView<'a> {
441475
Span::styled(" ⎿ ", Style::default().fg(self.colors.text_muted)),
442476
Span::styled("Error: ", Style::default().fg(self.colors.error)),
443477
]));
444-
// Display error message, truncate if too long
445-
for (i, err_line) in error_msg.lines().take(5).enumerate() {
446-
let truncated = if err_line.len() > 70 {
447-
format!("{}...", &err_line.chars().take(67).collect::<String>())
448-
} else {
449-
err_line.to_string()
450-
};
451-
let prefix = if i == 0 { " " } else { " " };
452-
lines.push(Line::from(vec![
453-
Span::styled(prefix, Style::default().fg(self.colors.text_muted)),
454-
Span::styled(truncated, Style::default().fg(self.colors.error)),
455-
]));
478+
// Display error message with wrapping
479+
for err_line in error_msg.lines().take(5) {
480+
let wrapped = Self::wrap_text(err_line, line_width.saturating_sub(4));
481+
for (i, wrapped_line) in wrapped.iter().enumerate() {
482+
let prefix = if i == 0 { " " } else { " " };
483+
lines.push(Line::from(vec![
484+
Span::styled(prefix, Style::default().fg(self.colors.text_muted)),
485+
Span::styled(
486+
wrapped_line.clone(),
487+
Style::default().fg(self.colors.error),
488+
),
489+
]));
490+
}
456491
}
457492
} else {
458493
// Fallback: no error message provided
@@ -469,9 +504,17 @@ impl<'a> MinimalSessionView<'a> {
469504
SubagentTodoStatus::InProgress => ("[in_progress]", self.colors.accent),
470505
SubagentTodoStatus::Pending => ("[pending]", self.colors.text_muted),
471506
};
472-
// Truncate long todo content
473-
let content = if todo.content.len() > 50 {
474-
format!("{}...", &todo.content.chars().take(47).collect::<String>())
507+
// Calculate max content width (accounting for status text)
508+
let max_content = content_width.saturating_sub(status_text.len() + 1);
509+
let content = if todo.content.len() > max_content {
510+
format!(
511+
"{}...",
512+
&todo
513+
.content
514+
.chars()
515+
.take(max_content.saturating_sub(3))
516+
.collect::<String>()
517+
)
475518
} else {
476519
todo.content.clone()
477520
};
@@ -485,13 +528,17 @@ impl<'a> MinimalSessionView<'a> {
485528
]));
486529
}
487530
} else {
488-
// No todos yet - show current activity with ⎿ (truncate if too long)
531+
// No todos yet - show current activity with ⎿ (wrap if too long)
489532
let activity = if task.current_activity.is_empty() {
490533
"Initializing...".to_string()
491-
} else if task.current_activity.len() > 70 {
534+
} else if task.current_activity.len() > content_width {
492535
format!(
493536
"{}...",
494-
&task.current_activity.chars().take(67).collect::<String>()
537+
&task
538+
.current_activity
539+
.chars()
540+
.take(content_width.saturating_sub(3))
541+
.collect::<String>()
495542
)
496543
} else {
497544
task.current_activity.clone()
@@ -742,7 +789,7 @@ impl<'a> MinimalSessionView<'a> {
742789
.iter()
743790
.find(|c| &c.id == tool_call_id)
744791
{
745-
all_lines.extend(self.render_tool_call(call));
792+
all_lines.extend(self.render_tool_call(call, width));
746793
}
747794
}
748795
}
@@ -763,15 +810,15 @@ impl<'a> MinimalSessionView<'a> {
763810
}
764811

765812
for call in &sorted_calls {
766-
all_lines.extend(self.render_tool_call(call));
813+
all_lines.extend(self.render_tool_call(call, width));
767814
}
768815
} else if let Some(ref content) = streaming_content {
769816
all_lines.extend(self.render_streaming_content(content, width));
770817
}
771818

772819
// Render active subagents
773820
for task in &self.app_state.active_subagents {
774-
all_lines.extend(self.render_subagent(task));
821+
all_lines.extend(self.render_subagent(task, width));
775822
}
776823

777824
all_lines

0 commit comments

Comments
 (0)