Skip to repository content397 lines · 14.0 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T05:05:08.955Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
terminal_tool_header.rs
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