agent: Display full terminal output without scrolling (#31922)
The terminal tool card used a fixed height and scrolling, but this meant that it was too tall for commands that only outputted a few lines, and the nested scrolling was undesirable. This PR makes the card be as too as needed to fit the entire output (no scrolling), and allows the user to collapse it to fewer lines when applicable. Making it work the same way as the edit tool card. In fact, both tools now use a shared UI component. https://github.com/user-attachments/assets/1127e21d-1d41-4a4b-a99f-7cd70fccbb56 Release Notes: - Agent: Display full terminal output - Agent: Allow collapsing terminal output
This commit is contained in:
parent
01a77bb231
commit
b7abc9d493
9 changed files with 275 additions and 144 deletions
|
@ -2,6 +2,7 @@ use crate::{
|
|||
Templates,
|
||||
edit_agent::{EditAgent, EditAgentOutput, EditAgentOutputEvent},
|
||||
schema::json_schema_for,
|
||||
ui::{COLLAPSED_LINES, ToolOutputPreview},
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{
|
||||
|
@ -13,7 +14,7 @@ use editor::{Editor, EditorMode, MinimapVisibility, MultiBuffer, PathKey};
|
|||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
Animation, AnimationExt, AnyWindowHandle, App, AppContext, AsyncApp, Entity, Task,
|
||||
TextStyleRefinement, WeakEntity, pulsating_between,
|
||||
TextStyleRefinement, WeakEntity, pulsating_between, px,
|
||||
};
|
||||
use indoc::formatdoc;
|
||||
use language::{
|
||||
|
@ -884,30 +885,8 @@ impl ToolCard for EditFileToolCard {
|
|||
(element.into_any_element(), line_height)
|
||||
});
|
||||
|
||||
let (full_height_icon, full_height_tooltip_label) = if self.full_height_expanded {
|
||||
(IconName::ChevronUp, "Collapse Code Block")
|
||||
} else {
|
||||
(IconName::ChevronDown, "Expand Code Block")
|
||||
};
|
||||
|
||||
let gradient_overlay =
|
||||
div()
|
||||
.absolute()
|
||||
.bottom_0()
|
||||
.left_0()
|
||||
.w_full()
|
||||
.h_2_5()
|
||||
.bg(gpui::linear_gradient(
|
||||
0.,
|
||||
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
|
||||
gpui::linear_color_stop(cx.theme().colors().editor_background.opacity(0.), 1.),
|
||||
));
|
||||
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
|
||||
const DEFAULT_COLLAPSED_LINES: u32 = 10;
|
||||
let is_collapsible = self.total_lines.unwrap_or(0) > DEFAULT_COLLAPSED_LINES;
|
||||
|
||||
let waiting_for_diff = {
|
||||
let styles = [
|
||||
("w_4_5", (0.1, 0.85), 2000),
|
||||
|
@ -992,48 +971,34 @@ impl ToolCard for EditFileToolCard {
|
|||
card.child(waiting_for_diff)
|
||||
})
|
||||
.when(self.preview_expanded && !self.is_loading(), |card| {
|
||||
let editor_view = v_flex()
|
||||
.relative()
|
||||
.h_full()
|
||||
.when(!self.full_height_expanded, |editor_container| {
|
||||
editor_container.max_h(px(COLLAPSED_LINES as f32 * editor_line_height.0))
|
||||
})
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(editor);
|
||||
|
||||
card.child(
|
||||
v_flex()
|
||||
.relative()
|
||||
.h_full()
|
||||
.when(!self.full_height_expanded, |editor_container| {
|
||||
editor_container
|
||||
.max_h(DEFAULT_COLLAPSED_LINES as f32 * editor_line_height)
|
||||
})
|
||||
.overflow_hidden()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(editor)
|
||||
.when(
|
||||
!self.full_height_expanded && is_collapsible,
|
||||
|editor_container| editor_container.child(gradient_overlay),
|
||||
),
|
||||
ToolOutputPreview::new(editor_view.into_any_element(), self.editor.entity_id())
|
||||
.with_total_lines(self.total_lines.unwrap_or(0) as usize)
|
||||
.toggle_state(self.full_height_expanded)
|
||||
.with_collapsed_fade()
|
||||
.on_toggle({
|
||||
let this = cx.entity().downgrade();
|
||||
move |is_expanded, _window, cx| {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, _cx| {
|
||||
this.full_height_expanded = is_expanded;
|
||||
});
|
||||
}
|
||||
}
|
||||
}),
|
||||
)
|
||||
.when(is_collapsible, |card| {
|
||||
card.child(
|
||||
h_flex()
|
||||
.id(("expand-button", self.editor.entity_id()))
|
||||
.flex_none()
|
||||
.cursor_pointer()
|
||||
.h_5()
|
||||
.justify_center()
|
||||
.border_t_1()
|
||||
.rounded_b_md()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
|
||||
.child(
|
||||
Icon::new(full_height_icon)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.tooltip(Tooltip::text(full_height_tooltip_label))
|
||||
.on_click(cx.listener(move |this, _event, _window, _cx| {
|
||||
this.full_height_expanded = !this.full_height_expanded;
|
||||
})),
|
||||
)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,7 @@
|
|||
use crate::schema::json_schema_for;
|
||||
use crate::{
|
||||
schema::json_schema_for,
|
||||
ui::{COLLAPSED_LINES, ToolOutputPreview},
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use assistant_tool::{ActionLog, Tool, ToolCard, ToolResult, ToolUseStatus};
|
||||
use futures::{FutureExt as _, future::Shared};
|
||||
|
@ -25,7 +28,7 @@ use terminal_view::TerminalView;
|
|||
use theme::ThemeSettings;
|
||||
use ui::{Disclosure, Tooltip, prelude::*};
|
||||
use util::{
|
||||
get_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
|
||||
ResultExt, get_system_shell, markdown::MarkdownInlineCode, size::format_file_size,
|
||||
time::duration_alt_display,
|
||||
};
|
||||
use workspace::Workspace;
|
||||
|
@ -254,22 +257,24 @@ impl Tool for TerminalTool {
|
|||
|
||||
let terminal_view = window.update(cx, |_, window, cx| {
|
||||
cx.new(|cx| {
|
||||
TerminalView::new(
|
||||
let mut view = TerminalView::new(
|
||||
terminal.clone(),
|
||||
workspace.downgrade(),
|
||||
None,
|
||||
project.downgrade(),
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
);
|
||||
view.set_embedded_mode(None, cx);
|
||||
view
|
||||
})
|
||||
})?;
|
||||
|
||||
let _ = card.update(cx, |card, _| {
|
||||
card.update(cx, |card, _| {
|
||||
card.terminal = Some(terminal_view.clone());
|
||||
card.start_instant = Instant::now();
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
|
||||
let exit_status = terminal
|
||||
.update(cx, |terminal, cx| terminal.wait_for_completed_task(cx))?
|
||||
|
@ -285,7 +290,7 @@ impl Tool for TerminalTool {
|
|||
exit_status.map(portable_pty::ExitStatus::from),
|
||||
);
|
||||
|
||||
let _ = card.update(cx, |card, _| {
|
||||
card.update(cx, |card, _| {
|
||||
card.command_finished = true;
|
||||
card.exit_status = exit_status;
|
||||
card.was_content_truncated = processed_content.len() < previous_len;
|
||||
|
@ -293,7 +298,8 @@ impl Tool for TerminalTool {
|
|||
card.content_line_count = content_line_count;
|
||||
card.finished_with_empty_output = finished_with_empty_output;
|
||||
card.elapsed_time = Some(card.start_instant.elapsed());
|
||||
});
|
||||
})
|
||||
.log_err();
|
||||
|
||||
Ok(processed_content.into())
|
||||
}
|
||||
|
@ -473,7 +479,6 @@ impl ToolCard for TerminalToolCard {
|
|||
let time_elapsed = self
|
||||
.elapsed_time
|
||||
.unwrap_or_else(|| self.start_instant.elapsed());
|
||||
let should_hide_terminal = tool_failed || self.finished_with_empty_output;
|
||||
|
||||
let header_bg = cx
|
||||
.theme()
|
||||
|
@ -574,7 +579,7 @@ impl ToolCard for TerminalToolCard {
|
|||
),
|
||||
)
|
||||
})
|
||||
.when(!should_hide_terminal, |header| {
|
||||
.when(!self.finished_with_empty_output, |header| {
|
||||
header.child(
|
||||
Disclosure::new(
|
||||
("terminal-tool-disclosure", self.entity_id),
|
||||
|
@ -618,19 +623,43 @@ impl ToolCard for TerminalToolCard {
|
|||
),
|
||||
),
|
||||
)
|
||||
.when(self.preview_expanded && !should_hide_terminal, |this| {
|
||||
this.child(
|
||||
div()
|
||||
.pt_2()
|
||||
.min_h_72()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_b_md()
|
||||
.text_ui_sm(cx)
|
||||
.child(terminal.clone()),
|
||||
)
|
||||
})
|
||||
.when(
|
||||
self.preview_expanded && !self.finished_with_empty_output,
|
||||
|this| {
|
||||
this.child(
|
||||
div()
|
||||
.pt_2()
|
||||
.border_t_1()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.rounded_b_md()
|
||||
.text_ui_sm(cx)
|
||||
.child(
|
||||
ToolOutputPreview::new(
|
||||
terminal.clone().into_any_element(),
|
||||
terminal.entity_id(),
|
||||
)
|
||||
.with_total_lines(self.content_line_count)
|
||||
.toggle_state(!terminal.read(cx).is_content_limited(window))
|
||||
.on_toggle({
|
||||
let terminal = terminal.clone();
|
||||
move |is_expanded, _, cx| {
|
||||
terminal.update(cx, |terminal, cx| {
|
||||
terminal.set_embedded_mode(
|
||||
if is_expanded {
|
||||
None
|
||||
} else {
|
||||
Some(COLLAPSED_LINES)
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
},
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
mod tool_call_card_header;
|
||||
mod tool_output_preview;
|
||||
|
||||
pub use tool_call_card_header::*;
|
||||
pub use tool_output_preview::*;
|
||||
|
|
115
crates/assistant_tools/src/ui/tool_output_preview.rs
Normal file
115
crates/assistant_tools/src/ui/tool_output_preview.rs
Normal file
|
@ -0,0 +1,115 @@
|
|||
use gpui::{AnyElement, EntityId, prelude::*};
|
||||
use ui::{Tooltip, prelude::*};
|
||||
|
||||
#[derive(IntoElement)]
|
||||
pub struct ToolOutputPreview<F>
|
||||
where
|
||||
F: Fn(bool, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
content: AnyElement,
|
||||
entity_id: EntityId,
|
||||
full_height: bool,
|
||||
total_lines: usize,
|
||||
collapsed_fade: bool,
|
||||
on_toggle: Option<F>,
|
||||
}
|
||||
|
||||
pub const COLLAPSED_LINES: usize = 10;
|
||||
|
||||
impl<F> ToolOutputPreview<F>
|
||||
where
|
||||
F: Fn(bool, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
pub fn new(content: AnyElement, entity_id: EntityId) -> Self {
|
||||
Self {
|
||||
content,
|
||||
entity_id,
|
||||
full_height: true,
|
||||
total_lines: 0,
|
||||
collapsed_fade: false,
|
||||
on_toggle: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_total_lines(mut self, total_lines: usize) -> Self {
|
||||
self.total_lines = total_lines;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn toggle_state(mut self, full_height: bool) -> Self {
|
||||
self.full_height = full_height;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_collapsed_fade(mut self) -> Self {
|
||||
self.collapsed_fade = true;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn on_toggle(mut self, listener: F) -> Self {
|
||||
self.on_toggle = Some(listener);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> RenderOnce for ToolOutputPreview<F>
|
||||
where
|
||||
F: Fn(bool, &mut Window, &mut App) + 'static,
|
||||
{
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
if self.total_lines <= COLLAPSED_LINES {
|
||||
return self.content;
|
||||
}
|
||||
let border_color = cx.theme().colors().border.opacity(0.6);
|
||||
|
||||
let (icon, tooltip_label) = if self.full_height {
|
||||
(IconName::ChevronUp, "Collapse")
|
||||
} else {
|
||||
(IconName::ChevronDown, "Expand")
|
||||
};
|
||||
|
||||
let gradient_overlay =
|
||||
if self.collapsed_fade && !self.full_height {
|
||||
Some(div().absolute().bottom_5().left_0().w_full().h_2_5().bg(
|
||||
gpui::linear_gradient(
|
||||
0.,
|
||||
gpui::linear_color_stop(cx.theme().colors().editor_background, 0.),
|
||||
gpui::linear_color_stop(
|
||||
cx.theme().colors().editor_background.opacity(0.),
|
||||
1.,
|
||||
),
|
||||
),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.relative()
|
||||
.child(self.content)
|
||||
.children(gradient_overlay)
|
||||
.child(
|
||||
h_flex()
|
||||
.id(("expand-button", self.entity_id))
|
||||
.flex_none()
|
||||
.cursor_pointer()
|
||||
.h_5()
|
||||
.justify_center()
|
||||
.border_t_1()
|
||||
.rounded_b_md()
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.hover(|style| style.bg(cx.theme().colors().element_hover.opacity(0.1)))
|
||||
.child(Icon::new(icon).size(IconSize::Small).color(Color::Muted))
|
||||
.tooltip(Tooltip::text(tooltip_label))
|
||||
.when_some(self.on_toggle, |this, on_toggle| {
|
||||
this.on_click({
|
||||
move |_, window, cx| {
|
||||
on_toggle(!self.full_height, window, cx);
|
||||
}
|
||||
})
|
||||
}),
|
||||
)
|
||||
.into_any()
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue