Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T06:06:56.039Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

terminal_tool_header.rs

397 lines · 14.0 KB · rust
1use std::time::Duration;
2
3use gpui::{AnyElement, ClickEvent, CursorStyle, Window};
4use ui::{CommonAnimationExt, Disclosure, Divider, DividerColor, Tooltip, prelude::*};
5use util::time::duration_alt_display;
6
7const ELAPSED_DISPLAY_THRESHOLD: Duration = Duration::from_secs(10);
8
9type ClickHandler = Box<dyn Fn(&ClickEvent, &mut Window, &mut App) + 'static>;
10
11pub struct TerminalSandboxWarning {
12    pub title: SharedString,
13    pub detail: SharedString,
14    pub docs_url: SharedString,
15}
16
17#[derive(IntoElement, RegisterComponent)]
18pub struct TerminalToolHeader {
19    id: SharedString,
20    hover_group: SharedString,
21    working_dir: SharedString,
22    is_expanded: bool,
23    elapsed: Option<Duration>,
24    running: bool,
25    truncated_tooltip: Option<SharedString>,
26    failed: bool,
27    exit_code: Option<i32>,
28    sandbox_warning: Option<TerminalSandboxWarning>,
29    on_toggle_expand: Option<ClickHandler>,
30    on_stop: Option<ClickHandler>,
31    command_slot: Option<AnyElement>,
32}
33
34impl TerminalToolHeader {
35    pub fn new(
36        id: impl Into<SharedString>,
37        hover_group: impl Into<SharedString>,
38        working_dir: impl Into<SharedString>,
39        is_expanded: bool,
40    ) -> Self {
41        Self {
42            id: id.into(),
43            hover_group: hover_group.into(),
44            working_dir: working_dir.into(),
45            is_expanded,
46            elapsed: None,
47            running: false,
48            truncated_tooltip: None,
49            failed: false,
50            exit_code: None,
51            sandbox_warning: None,
52            on_toggle_expand: None,
53            on_stop: None,
54            command_slot: None,
55        }
56    }
57
58    pub fn elapsed(mut self, elapsed: Duration) -> Self {
59        self.elapsed = Some(elapsed);
60        self
61    }
62
63    pub fn running(mut self, running: bool) -> Self {
64        self.running = running;
65        self
66    }
67
68    pub fn truncated(mut self, tooltip: impl Into<SharedString>) -> Self {
69        self.truncated_tooltip = Some(tooltip.into());
70        self
71    }
72
73    pub fn failed(mut self, exit_code: Option<i32>) -> Self {
74        self.failed = true;
75        self.exit_code = exit_code;
76        self
77    }
78
79    pub fn sandbox_warning(mut self, warning: TerminalSandboxWarning) -> Self {
80        self.sandbox_warning = Some(warning);
81        self
82    }
83
84    pub fn on_toggle_expand(
85        mut self,
86        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
87    ) -> Self {
88        self.on_toggle_expand = Some(Box::new(handler));
89        self
90    }
91
92    pub fn on_stop(
93        mut self,
94        handler: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
95    ) -> Self {
96        self.on_stop = Some(Box::new(handler));
97        self
98    }
99
100    pub fn command_slot(mut self, element: impl IntoElement) -> Self {
101        self.command_slot = Some(element.into_any_element());
102        self
103    }
104}
105
106impl RenderOnce for TerminalToolHeader {
107    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
108        let show_elapsed = self
109            .elapsed
110            .is_some_and(|elapsed| elapsed > ELAPSED_DISPLAY_THRESHOLD);
111
112        let Self {
113            id,
114            hover_group,
115            working_dir,
116            is_expanded,
117            elapsed,
118            running,
119            truncated_tooltip,
120            failed,
121            exit_code,
122            sandbox_warning,
123            on_toggle_expand,
124            on_stop,
125            command_slot,
126        } = self;
127
128        let child_id = |name: &str| format!("terminal-tool-{name}-{id}");
129
130        let header_bg = cx
131            .theme()
132            .colors()
133            .element_background
134            .blend(cx.theme().colors().editor_foreground.opacity(0.025));
135
136        let header_row = h_flex()
137            .id(child_id("header"))
138            .pt_1()
139            .pl_1p5()
140            .pr_1()
141            .flex_none()
142            .gap_1()
143            .justify_between()
144            .rounded_t_md()
145            .child(
146                div().w_full().min_w_0().overflow_hidden().child(
147                    Label::new(working_dir)
148                        .buffer_font(cx)
149                        .size(LabelSize::XSmall)
150                        .color(Color::Muted)
151                        .truncate_start(),
152                ),
153            )
154            .child(
155                Disclosure::new(child_id("disclosure"), is_expanded)
156                    .opened_icon(IconName::ChevronUp)
157                    .closed_icon(IconName::ChevronDown)
158                    .visible_on_hover(&hover_group)
159                    .when_some(on_toggle_expand, |this, handler| this.on_click(handler)),
160            )
161            .when(show_elapsed, |header| {
162                let elapsed = elapsed.unwrap_or_default();
163                header.child(
164                    Label::new(format!("({})", duration_alt_display(elapsed)))
165                        .buffer_font(cx)
166                        .color(Color::Muted)
167                        .size(LabelSize::XSmall)
168                        .mr_0p5()
169                        .when(truncated_tooltip.is_some(), |s| s.mr_0()),
170                )
171            })
172            .when(running, |header| {
173                header
174                    .gap_1p5()
175                    .child(
176                        Icon::new(IconName::LoadCircle)
177                            .size(IconSize::Small)
178                            .color(Color::Muted)
179                            .with_rotate_animation(2),
180                    )
181                    .child(Divider::vertical().color(DividerColor::Border).ml_1())
182                    .child(
183                        IconButton::new(child_id("stop"), IconName::Stop)
184                            .icon_size(IconSize::Small)
185                            .icon_color(Color::Error)
186                            .tooltip(move |_window, cx| {
187                                Tooltip::with_meta(
188                                    "Stop This Command",
189                                    None,
190                                    "Also possible by placing your cursor inside the terminal \
191                                     and using regular terminal bindings.",
192                                    cx,
193                                )
194                            })
195                            .when_some(on_stop, |this, handler| this.on_click(handler)),
196                    )
197            })
198            .when_some(truncated_tooltip, |header, tooltip| {
199                header.child(
200                    IconButton::new(child_id("truncated"), IconName::Info)
201                        .cursor_style(CursorStyle::Arrow)
202                        .style(ButtonStyle::Transparent)
203                        .icon_size(IconSize::Small)
204                        .icon_color(Color::Muted)
205                        .tooltip(Tooltip::text(tooltip)),
206                )
207            })
208            .when(failed, |header| {
209                header.child(
210                    IconButton::new(child_id("failed"), IconName::Close)
211                        .cursor_style(CursorStyle::Arrow)
212                        .style(ButtonStyle::Transparent)
213                        .icon_size(IconSize::Small)
214                        .icon_color(Color::Error)
215                        .when_some(exit_code, |this, code| {
216                            this.tooltip(Tooltip::text(format!("Exited with code {code}")))
217                        }),
218                )
219            })
220            .when_some(sandbox_warning, |header, warning| {
221                let TerminalSandboxWarning {
222                    title,
223                    detail,
224                    docs_url,
225                } = warning;
226                header.child(
227                    IconButton::new(child_id("sandbox-not-applied"), IconName::LockOff)
228                        .icon_size(IconSize::Small)
229                        .tooltip(move |_window, cx| {
230                            Tooltip::with_meta(
231                                title.clone(),
232                                None,
233                                format!("{detail} Click to learn more about sandboxing."),
234                                cx,
235                            )
236                        })
237                        .on_click(move |_, _, cx| cx.open_url(&docs_url)),
238                )
239            });
240
241        v_flex()
242            .group(hover_group)
243            .text_xs()
244            .bg(header_bg)
245            .child(header_row)
246            .children(command_slot)
247    }
248}
249
250impl Component for TerminalToolHeader {
251    fn scope() -> ComponentScope {
252        ComponentScope::Agent
253    }
254
255    fn name() -> &'static str {
256        "Terminal Tool Header"
257    }
258
259    fn description() -> &'static str {
260        "The top of a terminal tool call card in the agent panel."
261    }
262
263    fn preview(_window: &mut Window, cx: &mut App) -> AnyElement {
264        let working_dir = "/Users/you/projects/zed";
265
266        let card = |_id: &'static str, header: TerminalToolHeader| {
267            v_flex()
268                .w_full()
269                .border_1()
270                .border_color(cx.theme().colors().border.opacity(0.6))
271                .rounded_md()
272                .overflow_hidden()
273                .child(
274                    header.command_slot(
275                        div().px_1p5().pb_1().child(
276                            Label::new("cargo build --release")
277                                .buffer_font(cx)
278                                .size(LabelSize::XSmall),
279                        ),
280                    ),
281                )
282                .into_any_element()
283        };
284
285        let sandbox_warning = || TerminalSandboxWarning {
286            title: "Ran without sandbox".into(),
287            detail: "Unsandboxed execution is allowed for the rest of this thread.".into(),
288            docs_url: "https://zed.dev/docs/ai/sandboxing".into(),
289        };
290
291        v_flex()
292            .gap_4()
293            .child(example_group(vec![
294                single_example(
295                    "Running",
296                    card(
297                        "running",
298                        TerminalToolHeader::new(
299                            "running",
300                            "preview-terminal-header-group-running",
301                            working_dir,
302                            false,
303                        )
304                        .running(true),
305                    ),
306                ),
307                single_example(
308                    "Finished (long-running)",
309                    card(
310                        "elapsed",
311                        TerminalToolHeader::new(
312                            "elapsed",
313                            "preview-terminal-header-group-elapsed",
314                            working_dir,
315                            false,
316                        )
317                        .elapsed(Duration::from_secs(83)),
318                    ),
319                ),
320                single_example(
321                    "Truncated output",
322                    card(
323                        "truncated",
324                        TerminalToolHeader::new(
325                            "truncated",
326                            "preview-terminal-header-group-truncated",
327                            working_dir,
328                            true,
329                        )
330                        .truncated(
331                            "Output is 2.5 MB long, and to avoid unexpected token \
332                                     usage, only 16 KB was sent back to the agent.",
333                        ),
334                    ),
335                ),
336                single_example(
337                    "Failed with exit code",
338                    card(
339                        "failed",
340                        TerminalToolHeader::new(
341                            "failed",
342                            "preview-terminal-header-group-failed",
343                            working_dir,
344                            false,
345                        )
346                        .failed(Some(101)),
347                    ),
348                ),
349                single_example(
350                    "Ran without sandbox",
351                    card(
352                        "sandbox",
353                        TerminalToolHeader::new(
354                            "sandbox",
355                            "preview-terminal-header-group-sandbox",
356                            working_dir,
357                            false,
358                        )
359                        .sandbox_warning(sandbox_warning()),
360                    ),
361                ),
362                single_example(
363                    "Long path (truncated from the start)",
364                    div()
365                        .w_80()
366                        .child(card(
367                            "long-path",
368                            TerminalToolHeader::new(
369                                "long-path",
370                                "preview-terminal-header-group-long-path",
371                                "/Users/you/Documents/GitHub/worktrees/some-monorepo/working-tree-three/packages/deeply/nested/service/backend/src",
372                                false,
373                            ),
374                        ))
375                        .into_any_element(),
376                ),
377                single_example(
378                    "Everything at once",
379                    card(
380                        "kitchen-sink",
381                        TerminalToolHeader::new(
382                            "kitchen-sink",
383                            "preview-terminal-header-group-kitchen-sink",
384                            working_dir,
385                            true,
386                        )
387                        .elapsed(Duration::from_secs(3671))
388                        .truncated("Output was truncated")
389                        .failed(Some(1))
390                        .sandbox_warning(sandbox_warning()),
391                    ),
392                ),
393            ]))
394            .into_any_element()
395    }
396}
397
Served at tenant.openagents/omega Member data and write actions are omitted.