396 lines · 13.7 KB · Rust
RawDownload
use std::time::Duration; use gpui::{AnyElement, ClickEvent, CursorStyle, Window};use ui::{CommonAnimationExt, Disclosure, Divider, DividerColor, Tooltip, prelude::*};use util::time::duration_alt_display; const ELAPSED_DISPLAY_THRESHOLD: Duration = Duration::from_secs(10); type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>; pub struct TerminalSandboxWarning {    pub title: SharedString,    pub detail: SharedString,    pub docs_url: SharedString,} #[derive(IntoElement, RegisterComponent)]pub struct TerminalToolHeader {    id: SharedString,    hover_group: SharedString,    working_dir: SharedString,    is_expanded: bool,    elapsed: Option<Duration>,    running: bool,    truncated_tooltip: Option<SharedString>,    failed: bool,    exit_code: Option<i32>,    sandbox_warning: Option<TerminalSandboxWarning>,    on_toggle_expand: Option<ClickHandler>,    on_stop: Option<ClickHandler>,    command_slot: Option<AnyElement>,} impl TerminalToolHeader {    pub fn new(        id: impl Into<SharedString>,        hover_group: impl Into<SharedString>,        working_dir: impl Into<SharedString>,        is_expanded: bool,    ) -> Self {        Self {            id: id.into(),            hover_group: hover_group.into(),            working_dir: working_dir.into(),            is_expanded,            elapsed: None,            running: false,            truncated_tooltip: None,            failed: false,            exit_code: None,            sandbox_warning: None,            on_toggle_expand: None,            on_stop: None,            command_slot: None,        }    }     pub fn elapsed(mut self, elapsed: Duration) -> Self {        self.elapsed = Some(elapsed);        self    }     pub fn running(mut self, running: bool) -> Self {        self.running = running;        self    }     pub fn truncated(mut self, tooltip: impl Into<SharedString>) -> Self {        self.truncated_tooltip = Some(tooltip.into());        self    }     pub fn failed(mut self, exit_code: Option<i32>) -> Self {        self.failed = true;        self.exit_code = exit_code;        self    }     pub fn sandbox_warning(mut self, warning: TerminalSandboxWarning) -> Self {        self.sandbox_warning = Some(warning);        self    }     pub fn on_toggle_expand(        mut self,        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,    ) -> Self {        self.on_toggle_expand = Some(Box::new(handler));        self    }     pub fn on_stop(        mut self,        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,    ) -> Self {        self.on_stop = Some(Box::new(handler));        self    }     pub fn command_slot(mut self, element: impl IntoElement) -> Self {        self.command_slot = Some(element.into_any_element());        self    }} impl RenderOnce for TerminalToolHeader {    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {        let show_elapsed = self            .elapsed            .is_some_and(|elapsed| elapsed > ELAPSED_DISPLAY_THRESHOLD);         let Self {            id,            hover_group,            working_dir,            is_expanded,            elapsed,            running,            truncated_tooltip,            failed,            exit_code,            sandbox_warning,            on_toggle_expand,            on_stop,            command_slot,        } = self;         let child_id = |name: &str| format!("terminal-tool-{name}-{id}");         let header_bg = cx            .theme()            .colors()            .element_background            .blend(cx.theme().colors().editor_foreground.opacity(0.025));         let header_row = h_flex()            .id(child_id("header"))            .pt_1()            .pl_1p5()            .pr_1()            .flex_none()            .gap_1()            .justify_between()            .rounded_t_md()            .child(                div().w_full().min_w_0().overflow_hidden().child(                    Label::new(working_dir)                        .buffer_font(cx)                        .size(LabelSize::XSmall)                        .color(Color::Muted)                        .truncate_start(),                ),            )            .child(                Disclosure::new(child_id("disclosure"), is_expanded)                    .opened_icon(IconName::ChevronUp)                    .closed_icon(IconName::ChevronDown)                    .visible_on_hover(&hover_group)                    .when_some(on_toggle_expand, |this, handler| this.on_click(handler)),            )            .when(show_elapsed, |header| {                let elapsed = elapsed.unwrap_or_default();                header.child(                    Label::new(format!("({})", duration_alt_display(elapsed)))                        .buffer_font(cx)                        .color(Color::Muted)                        .size(LabelSize::XSmall)                        .mr_0p5()                        .when(truncated_tooltip.is_some(), |s| s.mr_0()),                )            })            .when(running, |header| {                header                    .gap_1p5()                    .child(                        Icon::new(IconName::LoadCircle)                            .size(IconSize::Small)                            .color(Color::Muted)                            .with_rotate_animation(2),                    )                    .child(Divider::vertical().color(DividerColor::Border).ml_1())                    .child(                        IconButton::new(child_id("stop"), IconName::Stop)                            .icon_size(IconSize::Small)                            .icon_color(Color::Error)                            .tooltip(move |_window, cx| {                                Tooltip::with_meta(                                    "Stop This Command",                                    None,                                    "Also possible by placing your cursor inside the terminal \                                     and using regular terminal bindings.",                                    cx,                                )                            })                            .when_some(on_stop, |this, handler| this.on_click(handler)),                    )            })            .when_some(truncated_tooltip, |header, tooltip| {                header.child(                    IconButton::new(child_id("truncated"), IconName::Info)                        .cursor_style(CursorStyle::Arrow)                        .style(ButtonStyle::Transparent)                        .icon_size(IconSize::Small)                        .icon_color(Color::Muted)                        .tooltip(Tooltip::text(tooltip)),                )            })            .when(failed, |header| {                header.child(                    IconButton::new(child_id("failed"), IconName::Close)                        .cursor_style(CursorStyle::Arrow)                        .style(ButtonStyle::Transparent)                        .icon_size(IconSize::Small)                        .icon_color(Color::Error)                        .when_some(exit_code, |this, code| {                            this.tooltip(Tooltip::text(format!("Exited with code {code}")))                        }),                )            })            .when_some(sandbox_warning, |header, warning| {                let TerminalSandboxWarning {                    title,                    detail,                    docs_url,                } = warning;                header.child(                    IconButton::new(child_id("sandbox-not-applied"), IconName::LockOff)                        .icon_size(IconSize::Small)                        .tooltip(move |_window, cx| {                            Tooltip::with_meta(                                title.clone(),                                None,                                format!("{detail} Click to learn more about sandboxing."),                                cx,                            )                        })                        .on_click(move |_, _, cx| cx.open_url(&docs_url)),                )            });         v_flex()            .group(hover_group)            .text_xs()            .bg(header_bg)            .child(header_row)            .children(command_slot)    }} impl Component for TerminalToolHeader {    fn scope() -> ComponentScope {        ComponentScope::Agent    }     fn name() -> &'static str {        "Terminal Tool Header"    }     fn description() -> &'static str {        "The top of a terminal tool call card in the agent panel."    }     fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {        let working_dir = "/Users/you/projects/zed";         let card = |_id: &'static str, header: TerminalToolHeader| {            v_flex()                .w_full()                .border_1()                .border_color(cx.theme().colors().border.opacity(0.6))                .rounded_md()                .overflow_hidden()                .child(                    header.command_slot(                        div().px_1p5().pb_1().child(                            Label::new("cargo build --release")                                .buffer_font(cx)                                .size(LabelSize::XSmall),                        ),                    ),                )                .into_any_element()        };         let sandbox_warning = || TerminalSandboxWarning {            title: "Ran without sandbox".into(),            detail: "Unsandboxed execution is allowed for the rest of this thread.".into(),            docs_url: "https://zed.dev/docs/ai/sandboxing".into(),        };         v_flex()            .gap_4()            .child(example_group(vec![                single_example(                    "Running",                    card(                        "running",                        TerminalToolHeader::new(                            "running",                            "preview-terminal-header-group-running",                            working_dir,                            false,                        )                        .running(true),                    ),                ),                single_example(                    "Finished (long-running)",                    card(                        "elapsed",                        TerminalToolHeader::new(                            "elapsed",                            "preview-terminal-header-group-elapsed",                            working_dir,                            false,                        )                        .elapsed(Duration::from_secs(83)),                    ),                ),                single_example(                    "Truncated output",                    card(                        "truncated",                        TerminalToolHeader::new(                            "truncated",                            "preview-terminal-header-group-truncated",                            working_dir,                            true,                        )                        .truncated(                            "Output is 2.5 MB long, and to avoid unexpected token \                                     usage, only 16 KB was sent back to the agent.",                        ),                    ),                ),                single_example(                    "Failed with exit code",                    card(                        "failed",                        TerminalToolHeader::new(                            "failed",                            "preview-terminal-header-group-failed",                            working_dir,                            false,                        )                        .failed(Some(101)),                    ),                ),                single_example(                    "Ran without sandbox",                    card(                        "sandbox",                        TerminalToolHeader::new(                            "sandbox",                            "preview-terminal-header-group-sandbox",                            working_dir,                            false,                        )                        .sandbox_warning(sandbox_warning()),                    ),                ),                single_example(                    "Long path (truncated from the start)",                    div()                        .w_80()                        .child(card(                            "long-path",                            TerminalToolHeader::new(                                "long-path",                                "preview-terminal-header-group-long-path",                                "/Users/you/Documents/GitHub/worktrees/some-monorepo/working-tree-three/packages/deeply/nested/service/backend/src",                                false,                            ),                        ))                        .into_any_element(),                ),                single_example(                    "Everything at once",                    card(                        "kitchen-sink",                        TerminalToolHeader::new(                            "kitchen-sink",                            "preview-terminal-header-group-kitchen-sink",                            working_dir,                            true,                        )                        .elapsed(Duration::from_secs(3671))                        .truncated("Output was truncated")                        .failed(Some(1))                        .sandbox_warning(sandbox_warning()),                    ),                ),            ]))            .into_any_element()    }}