Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T03:58:24.059Z 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

activity_indicator.rs

782 lines · 32.3 KB · rust
1use auto_update::DismissMessage;
2use editor::Editor;
3use extension_host::{ExtensionOperation, ExtensionStore};
4use futures::StreamExt;
5use gpui::{
6    App, Context, Entity, EventEmitter, InteractiveElement as _, ParentElement as _, Render,
7    SharedString, Styled, Window, actions,
8};
9use language::{
10    BinaryStatus, LanguageRegistry, LanguageServerId, LanguageServerName,
11    LanguageServerStatusUpdate, ServerHealth,
12};
13use project::{
14    LanguageServerProgress, LspStoreEvent, ProgressToken, Project, ProjectEnvironmentEvent,
15    git_store::{GitStoreEvent, Repository},
16};
17use smallvec::SmallVec;
18use std::{
19    cmp::Reverse,
20    collections::HashSet,
21    fmt::Write,
22    sync::Arc,
23    time::{Duration, Instant},
24};
25use ui::{ContextMenu, PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*};
26use util::truncate_and_trailoff;
27use workspace::{StatusItemView, Workspace, item::ItemHandle};
28
29const GIT_OPERATION_DELAY: Duration = Duration::from_millis(0);
30
31actions!(
32    activity_indicator,
33    [
34        /// Displays error messages from language servers in the status bar.
35        ShowErrorMessage
36    ]
37);
38
39pub enum Event {
40    ShowStatus {
41        server_name: LanguageServerName,
42        status: SharedString,
43    },
44}
45
46pub struct ActivityIndicator {
47    statuses: Vec<ServerStatus>,
48    project: Entity<Project>,
49    context_menu_handle: PopoverMenuHandle<ContextMenu>,
50    fs_jobs: Vec<fs::JobInfo>,
51}
52
53#[derive(Debug)]
54struct ServerStatus {
55    name: LanguageServerName,
56    status: LanguageServerStatusUpdate,
57}
58
59struct PendingWork<'a> {
60    language_server_id: LanguageServerId,
61    progress_token: &'a ProgressToken,
62    progress: &'a LanguageServerProgress,
63}
64
65enum ActivityIcon {
66    LoadingSpinner,
67    Icon(IconName),
68}
69
70struct Content {
71    icon: ActivityIcon,
72    message: String,
73    on_click:
74        Option<Arc<dyn Fn(&mut ActivityIndicator, &mut Window, &mut Context<ActivityIndicator>)>>,
75    tooltip_message: Option<String>,
76}
77
78impl ActivityIndicator {
79    pub fn new(
80        workspace: &mut Workspace,
81        languages: Arc<LanguageRegistry>,
82        window: &mut Window,
83        cx: &mut Context<Workspace>,
84    ) -> Entity<ActivityIndicator> {
85        let project = workspace.project().clone();
86        let this = cx.new(|cx| {
87            let mut status_events = languages.language_server_binary_statuses();
88            cx.spawn(async move |this, cx| {
89                while let Some((name, binary_status)) = status_events.next().await {
90                    this.update(cx, |this: &mut ActivityIndicator, cx| {
91                        this.statuses.retain(|s| s.name != name);
92                        this.statuses.push(ServerStatus {
93                            name,
94                            status: LanguageServerStatusUpdate::Binary(binary_status),
95                        });
96                        cx.notify();
97                    })?;
98                }
99                anyhow::Ok(())
100            })
101            .detach();
102
103            let fs = project.read(cx).fs().clone();
104            let mut job_events = fs.subscribe_to_jobs();
105            cx.spawn(async move |this, cx| {
106                while let Some(job_event) = job_events.next().await {
107                    this.update(cx, |this: &mut ActivityIndicator, cx| {
108                        match job_event {
109                            fs::JobEvent::Started { info } => {
110                                this.fs_jobs.retain(|j| j.id != info.id);
111                                this.fs_jobs.push(info);
112                            }
113                            fs::JobEvent::Completed { id } => {
114                                this.fs_jobs.retain(|j| j.id != id);
115                            }
116                        }
117                        cx.notify();
118                    })?;
119                }
120                anyhow::Ok(())
121            })
122            .detach();
123
124            cx.subscribe(
125                &project.read(cx).lsp_store(),
126                |activity_indicator, _, event, cx| {
127                    if let LspStoreEvent::LanguageServerUpdate { name, message, .. } = event {
128                        if let proto::update_language_server::Variant::StatusUpdate(status_update) =
129                            message
130                        {
131                            let Some(name) = name.clone() else {
132                                return;
133                            };
134                            let status = match &status_update.status {
135                                Some(proto::status_update::Status::Binary(binary_status)) => {
136                                    if let Some(binary_status) =
137                                        proto::ServerBinaryStatus::from_i32(*binary_status)
138                                    {
139                                        let binary_status = match binary_status {
140                                            proto::ServerBinaryStatus::None => BinaryStatus::None,
141                                            proto::ServerBinaryStatus::CheckingForUpdate => {
142                                                BinaryStatus::CheckingForUpdate
143                                            }
144                                            proto::ServerBinaryStatus::Downloading => {
145                                                BinaryStatus::Downloading
146                                            }
147                                            proto::ServerBinaryStatus::Starting => {
148                                                BinaryStatus::Starting
149                                            }
150                                            proto::ServerBinaryStatus::Stopping => {
151                                                BinaryStatus::Stopping
152                                            }
153                                            proto::ServerBinaryStatus::Stopped => {
154                                                BinaryStatus::Stopped
155                                            }
156                                            proto::ServerBinaryStatus::Failed => {
157                                                let Some(error) = status_update.message.clone()
158                                                else {
159                                                    return;
160                                                };
161                                                BinaryStatus::Failed { error }
162                                            }
163                                        };
164                                        LanguageServerStatusUpdate::Binary(binary_status)
165                                    } else {
166                                        return;
167                                    }
168                                }
169                                Some(proto::status_update::Status::Health(health_status)) => {
170                                    if let Some(health) =
171                                        proto::ServerHealth::from_i32(*health_status)
172                                    {
173                                        let health = match health {
174                                            proto::ServerHealth::Ok => ServerHealth::Ok,
175                                            proto::ServerHealth::Warning => ServerHealth::Warning,
176                                            proto::ServerHealth::Error => ServerHealth::Error,
177                                        };
178                                        LanguageServerStatusUpdate::Health(
179                                            health,
180                                            status_update.message.clone().map(SharedString::from),
181                                        )
182                                    } else {
183                                        return;
184                                    }
185                                }
186                                None => return,
187                            };
188
189                            activity_indicator.statuses.retain(|s| s.name != name);
190                            activity_indicator
191                                .statuses
192                                .push(ServerStatus { name, status });
193                        }
194                        cx.notify()
195                    }
196                },
197            )
198            .detach();
199
200            cx.subscribe(
201                &project.read(cx).environment().clone(),
202                |_, _, event, cx| match event {
203                    ProjectEnvironmentEvent::ErrorsUpdated => cx.notify(),
204                },
205            )
206            .detach();
207
208            cx.subscribe(
209                &project.read(cx).git_store().clone(),
210                |_, _, event: &GitStoreEvent, cx| {
211                    if let project::git_store::GitStoreEvent::JobsUpdated = event {
212                        cx.notify()
213                    }
214                },
215            )
216            .detach();
217
218            Self {
219                statuses: Vec::new(),
220                project: project.clone(),
221                context_menu_handle: PopoverMenuHandle::default(),
222                fs_jobs: Vec::new(),
223            }
224        });
225
226        cx.subscribe_in(&this, window, move |_, _, event, window, cx| match event {
227            Event::ShowStatus {
228                server_name,
229                status,
230            } => {
231                let create_buffer =
232                    project.update(cx, |project, cx| project.create_buffer(None, false, cx));
233                let status = status.clone();
234                let server_name = server_name.clone();
235                cx.spawn_in(window, async move |workspace, cx| {
236                    let buffer = create_buffer.await?;
237                    buffer.update(cx, |buffer, cx| {
238                        buffer.edit(
239                            [(0..0, format!("Language server {server_name}:\n\n{status}"))],
240                            None,
241                            cx,
242                        );
243                        buffer.set_capability(language::Capability::ReadOnly, cx);
244                    });
245                    workspace.update_in(cx, |workspace, window, cx| {
246                        workspace.add_item_to_active_pane(
247                            Box::new(cx.new(|cx| {
248                                let mut editor = Editor::for_buffer(buffer, None, window, cx);
249                                editor.set_read_only(true);
250                                editor
251                            })),
252                            None,
253                            true,
254                            window,
255                            cx,
256                        );
257                    })?;
258
259                    anyhow::Ok(())
260                })
261                .detach();
262            }
263        })
264        .detach();
265        this
266    }
267
268    fn show_error_message(&mut self, _: &ShowErrorMessage, _: &mut Window, cx: &mut Context<Self>) {
269        let mut status_message_shown = false;
270        self.statuses.retain(|status| match &status.status {
271            LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { error })
272                if !status_message_shown =>
273            {
274                cx.emit(Event::ShowStatus {
275                    server_name: status.name.clone(),
276                    status: SharedString::from(error),
277                });
278                status_message_shown = true;
279                false
280            }
281            LanguageServerStatusUpdate::Health(
282                ServerHealth::Error | ServerHealth::Warning,
283                status_string,
284            ) if !status_message_shown => match status_string {
285                Some(error) => {
286                    cx.emit(Event::ShowStatus {
287                        server_name: status.name.clone(),
288                        status: error.clone(),
289                    });
290                    status_message_shown = true;
291                    false
292                }
293                None => false,
294            },
295            _ => true,
296        });
297    }
298
299    fn dismiss_message(&mut self, _: &DismissMessage, _: &mut Window, cx: &mut Context<Self>) {
300        self.project.update(cx, |project, cx| {
301            if project.last_formatting_failure(cx).is_some() {
302                project.reset_last_formatting_failure(cx);
303                true
304            } else {
305                false
306            }
307        });
308    }
309
310    fn pending_language_server_work<'a>(
311        &self,
312        cx: &'a App,
313    ) -> impl Iterator<Item = PendingWork<'a>> {
314        self.project
315            .read(cx)
316            .language_server_statuses(cx)
317            .rev()
318            .flat_map(|(server_id, status)| {
319                let mut pending_work = status
320                    .pending_work
321                    .iter()
322                    .map(|(progress_token, progress)| PendingWork {
323                        language_server_id: server_id,
324                        progress_token,
325                        progress,
326                    })
327                    .collect::<SmallVec<[_; 4]>>();
328                pending_work.sort_by_key(|work| Reverse(work.progress.last_update_at));
329                pending_work
330            })
331    }
332
333    fn pending_environment_error<'a>(&'a self, cx: &'a App) -> Option<&'a String> {
334        self.project.read(cx).peek_environment_error(cx)
335    }
336
337    fn content_to_render(&mut self, cx: &mut Context<Self>) -> Option<Content> {
338        // Show if any direnv calls failed
339        if let Some(message) = self.pending_environment_error(cx) {
340            return Some(Content {
341                icon: ActivityIcon::Icon(IconName::Warning),
342                message: message.clone(),
343                on_click: Some(Arc::new(move |this, window, cx| {
344                    this.project.update(cx, |project, cx| {
345                        project.pop_environment_error(cx);
346                    });
347                    window.dispatch_action(Box::new(workspace::OpenLog), cx);
348                })),
349                tooltip_message: None,
350            });
351        }
352        // Show any language server has pending activity.
353        {
354            let mut pending_work = self.pending_language_server_work(cx);
355            if let Some(PendingWork {
356                progress_token,
357                progress,
358                ..
359            }) = pending_work.next()
360            {
361                let mut message = progress.title.clone().unwrap_or(progress_token.to_string());
362
363                if let Some(percentage) = progress.percentage {
364                    write!(&mut message, " ({}%)", percentage).unwrap();
365                }
366
367                if let Some(progress_message) = progress.message.as_ref() {
368                    message.push_str(": ");
369                    message.push_str(progress_message);
370                }
371
372                let additional_work_count = pending_work.count();
373                if additional_work_count > 0 {
374                    write!(&mut message, " + {} more", additional_work_count).unwrap();
375                }
376
377                return Some(Content {
378                    icon: ActivityIcon::LoadingSpinner,
379                    message,
380                    on_click: None,
381                    tooltip_message: None,
382                });
383            }
384        }
385
386        if let Some(session) = self
387            .project
388            .read(cx)
389            .dap_store()
390            .read(cx)
391            .sessions()
392            .find(|s| !s.read(cx).is_started())
393        {
394            return Some(Content {
395                icon: ActivityIcon::LoadingSpinner,
396                message: format!("Debug: {}", session.read(cx).adapter()),
397                tooltip_message: session.read(cx).label().map(|label| label.to_string()),
398                on_click: None,
399            });
400        }
401
402        let current_job = self
403            .project
404            .read(cx)
405            .active_repository(cx)
406            .map(|r| r.read(cx))
407            .and_then(Repository::current_job);
408        // Show any long-running git command
409        if let Some(job_info) = current_job
410            && Instant::now() - job_info.start >= GIT_OPERATION_DELAY
411        {
412            return Some(Content {
413                icon: ActivityIcon::LoadingSpinner,
414                message: job_info.message.into(),
415                on_click: None,
416                tooltip_message: None,
417            });
418        }
419
420        // Show any long-running fs command
421        for fs_job in &self.fs_jobs {
422            if Instant::now().duration_since(fs_job.start) >= GIT_OPERATION_DELAY {
423                return Some(Content {
424                    icon: ActivityIcon::LoadingSpinner,
425                    message: fs_job.message.clone().into(),
426                    on_click: None,
427                    tooltip_message: None,
428                });
429            }
430        }
431
432        // Show any language server installation info.
433        let mut downloading = SmallVec::<[_; 3]>::new();
434        let mut checking_for_update = SmallVec::<[_; 3]>::new();
435        let mut failed = SmallVec::<[_; 3]>::new();
436        let mut health_messages = SmallVec::<[_; 3]>::new();
437        let mut servers_to_clear_statuses = HashSet::<LanguageServerName>::default();
438        for status in &self.statuses {
439            match &status.status {
440                LanguageServerStatusUpdate::Binary(
441                    BinaryStatus::Starting | BinaryStatus::Stopping,
442                ) => {}
443                LanguageServerStatusUpdate::Binary(BinaryStatus::Stopped) => {
444                    servers_to_clear_statuses.insert(status.name.clone());
445                }
446                LanguageServerStatusUpdate::Binary(BinaryStatus::CheckingForUpdate) => {
447                    checking_for_update.push(status.name.clone());
448                }
449                LanguageServerStatusUpdate::Binary(BinaryStatus::Downloading) => {
450                    downloading.push(status.name.clone());
451                }
452                LanguageServerStatusUpdate::Binary(BinaryStatus::Failed { .. }) => {
453                    failed.push(status.name.clone());
454                }
455                LanguageServerStatusUpdate::Binary(BinaryStatus::None) => {}
456                LanguageServerStatusUpdate::Health(health, server_status) => match server_status {
457                    Some(server_status) => {
458                        health_messages.push((status.name.clone(), *health, server_status.clone()));
459                    }
460                    None => {
461                        servers_to_clear_statuses.insert(status.name.clone());
462                    }
463                },
464            }
465        }
466        self.statuses
467            .retain(|status| !servers_to_clear_statuses.contains(&status.name));
468
469        health_messages.sort_by_key(|(_, health, _)| match health {
470            ServerHealth::Error => 2,
471            ServerHealth::Warning => 1,
472            ServerHealth::Ok => 0,
473        });
474
475        if !downloading.is_empty() {
476            return Some(Content {
477                icon: ActivityIcon::Icon(IconName::Download),
478                message: format!(
479                    "Downloading {}...",
480                    downloading.iter().map(|name| name.as_ref()).fold(
481                        String::new(),
482                        |mut acc, s| {
483                            if !acc.is_empty() {
484                                acc.push_str(", ");
485                            }
486                            acc.push_str(s);
487                            acc
488                        }
489                    )
490                ),
491                on_click: Some(Arc::new(move |this, window, cx| {
492                    this.statuses
493                        .retain(|status| !downloading.contains(&status.name));
494                    this.dismiss_message(&DismissMessage, window, cx)
495                })),
496                tooltip_message: None,
497            });
498        }
499
500        if !checking_for_update.is_empty() {
501            return Some(Content {
502                icon: ActivityIcon::Icon(IconName::Download),
503                message: format!(
504                    "Checking for updates to {}...",
505                    checking_for_update.iter().map(|name| name.as_ref()).fold(
506                        String::new(),
507                        |mut acc, s| {
508                            if !acc.is_empty() {
509                                acc.push_str(", ");
510                            }
511                            acc.push_str(s);
512                            acc
513                        }
514                    ),
515                ),
516                on_click: Some(Arc::new(move |this, window, cx| {
517                    this.statuses
518                        .retain(|status| !checking_for_update.contains(&status.name));
519                    this.dismiss_message(&DismissMessage, window, cx)
520                })),
521                tooltip_message: None,
522            });
523        }
524
525        if !failed.is_empty() {
526            return Some(Content {
527                icon: ActivityIcon::Icon(IconName::Warning),
528                message: format!(
529                    "Failed to run {}. Click to show error.",
530                    failed
531                        .iter()
532                        .map(|name| name.as_ref())
533                        .fold(String::new(), |mut acc, s| {
534                            if !acc.is_empty() {
535                                acc.push_str(", ");
536                            }
537                            acc.push_str(s);
538                            acc
539                        }),
540                ),
541                on_click: Some(Arc::new(|this, window, cx| {
542                    this.show_error_message(&ShowErrorMessage, window, cx)
543                })),
544                tooltip_message: None,
545            });
546        }
547
548        // Show any formatting failure
549        if let Some(failure) = self.project.read(cx).last_formatting_failure(cx) {
550            return Some(Content {
551                icon: ActivityIcon::Icon(IconName::Warning),
552                message: format!("Formatting failed: {failure}. Click to see logs."),
553                on_click: Some(Arc::new(|indicator, window, cx| {
554                    indicator.project.update(cx, |project, cx| {
555                        project.reset_last_formatting_failure(cx);
556                    });
557                    window.dispatch_action(Box::new(workspace::OpenLog), cx);
558                })),
559                tooltip_message: None,
560            });
561        }
562
563        // Show any health messages for the language servers
564        if let Some((server_name, health, message)) = health_messages.pop() {
565            let health_str = match health {
566                ServerHealth::Ok => format!("({server_name}) "),
567                ServerHealth::Warning => format!("({server_name}) Warning: "),
568                ServerHealth::Error => format!("({server_name}) Error: "),
569            };
570            let single_line_message = message
571                .lines()
572                .filter_map(|line| {
573                    let line = line.trim();
574                    if line.is_empty() { None } else { Some(line) }
575                })
576                .collect::<Vec<_>>()
577                .join(" ");
578            let mut altered_message = single_line_message != message;
579            let truncated_message = truncate_and_trailoff(
580                &single_line_message,
581                MAX_MESSAGE_LEN.saturating_sub(health_str.len()),
582            );
583            altered_message |= truncated_message != single_line_message;
584            let final_message = format!("{health_str}{truncated_message}");
585
586            let tooltip_message = if altered_message {
587                Some(format!("{health_str}{message}"))
588            } else {
589                None
590            };
591
592            return Some(Content {
593                icon: ActivityIcon::Icon(IconName::Warning),
594                message: final_message,
595                tooltip_message,
596                on_click: Some(Arc::new(move |activity_indicator, window, cx| {
597                    if altered_message {
598                        activity_indicator.show_error_message(&ShowErrorMessage, window, cx)
599                    } else {
600                        activity_indicator
601                            .statuses
602                            .retain(|status| status.name != server_name);
603                        cx.notify();
604                    }
605                })),
606            });
607        }
608
609        // Show any extension installation info.
610        if let Some(extension_store) =
611            ExtensionStore::try_global(cx).map(|extension_store| extension_store.read(cx))
612            && let Some((extension_id, operation)) =
613                extension_store.outstanding_operations().iter().next()
614        {
615            let (message, icon) = match operation {
616                ExtensionOperation::Install => (
617                    format!("Installing {extension_id} extension…"),
618                    ActivityIcon::LoadingSpinner,
619                ),
620                ExtensionOperation::Upgrade => (
621                    format!("Updating {extension_id} extension…"),
622                    ActivityIcon::Icon(IconName::Download),
623                ),
624                ExtensionOperation::Remove => (
625                    format!("Removing {extension_id} extension…"),
626                    ActivityIcon::LoadingSpinner,
627                ),
628            };
629
630            return Some(Content {
631                icon,
632                message,
633                on_click: Some(Arc::new(|this, window, cx| {
634                    this.dismiss_message(&Default::default(), window, cx)
635                })),
636                tooltip_message: None,
637            });
638        }
639
640        None
641    }
642}
643
644impl EventEmitter<Event> for ActivityIndicator {}
645
646const MAX_MESSAGE_LEN: usize = 50;
647
648impl Render for ActivityIndicator {
649    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
650        let result = h_flex()
651            .id("activity-indicator")
652            .on_action(cx.listener(Self::show_error_message))
653            .on_action(cx.listener(Self::dismiss_message));
654
655        let Some(content) = self.content_to_render(cx) else {
656            return result;
657        };
658
659        let activity_indicator = cx.entity().downgrade();
660        let truncate_content = content.message.len() > MAX_MESSAGE_LEN;
661        let has_click_handler = content.on_click.is_some();
662
663        result.child(
664            PopoverMenu::new("activity-indicator-popover")
665                .trigger(
666                    Button::new("activity-indicator-trigger", {
667                        if truncate_content {
668                            truncate_and_trailoff(&content.message, MAX_MESSAGE_LEN)
669                        } else {
670                            content.message.clone()
671                        }
672                    })
673                    .label_size(LabelSize::Small)
674                    .tab_index(0isize)
675                    .map(|this| match content.icon {
676                        ActivityIcon::LoadingSpinner => this.loading(true),
677                        ActivityIcon::Icon(icon_name) => this.start_icon(
678                            Icon::new(icon_name)
679                                .size(IconSize::Small)
680                                .color(Color::Muted),
681                        ),
682                    })
683                    .map(|button| {
684                        if truncate_content {
685                            button.tooltip(Tooltip::text(content.message))
686                        } else {
687                            button.when_some(content.tooltip_message, |this, tooltip_message| {
688                                this.tooltip(Tooltip::text(tooltip_message))
689                            })
690                        }
691                    })
692                    .when_some(content.on_click, |this, handler| {
693                        this.on_click(cx.listener(move |this, _, window, cx| {
694                            handler(this, window, cx);
695                        }))
696                    }),
697                )
698                .anchor(gpui::Anchor::BottomLeft)
699                .when(!has_click_handler, |this| {
700                    this.menu(move |window, cx| {
701                        let strong_this = activity_indicator.upgrade()?;
702                        let mut has_cancellable_work = false;
703                        let menu = ContextMenu::build(window, cx, |mut menu, _, cx| {
704                            for work in strong_this.read(cx).pending_language_server_work(cx) {
705                                let activity_indicator = activity_indicator.clone();
706                                let mut title = work
707                                    .progress
708                                    .title
709                                    .clone()
710                                    .unwrap_or(work.progress_token.to_string());
711
712                                if work.progress.is_cancellable {
713                                    has_cancellable_work = true;
714                                    let language_server_id = work.language_server_id;
715                                    let token = work.progress_token.clone();
716                                    let title = SharedString::from(format!("Cancel {title}"));
717                                    menu = menu.custom_entry(
718                                        move |_, _| {
719                                            h_flex()
720                                                .w_full()
721                                                .gap_1()
722                                                .child(
723                                                    Icon::new(IconName::Close)
724                                                        .color(Color::Muted)
725                                                        .size(IconSize::Small),
726                                                )
727                                                .child(Label::new(title.clone()))
728                                                .into_any_element()
729                                        },
730                                        move |_, cx| {
731                                            let token = token.clone();
732                                            activity_indicator
733                                                .update(cx, |activity_indicator, cx| {
734                                                    activity_indicator.project.update(
735                                                        cx,
736                                                        |project, cx| {
737                                                            project.cancel_language_server_work(
738                                                                language_server_id,
739                                                                Some(token),
740                                                                cx,
741                                                            );
742                                                        },
743                                                    );
744                                                    activity_indicator.context_menu_handle.hide(cx);
745                                                    cx.notify();
746                                                })
747                                                .ok();
748                                        },
749                                    );
750                                } else {
751                                    if let Some(progress_message) = work.progress.message.as_ref() {
752                                        title.push_str(": ");
753                                        title.push_str(progress_message);
754                                    }
755
756                                    menu = menu.label(title);
757                                }
758                            }
759                            menu
760                        });
761                        has_cancellable_work.then_some(menu)
762                    })
763                }),
764        )
765    }
766}
767
768impl StatusItemView for ActivityIndicator {
769    fn set_active_pane_item(
770        &mut self,
771        _: Option<&dyn ItemHandle>,
772        _window: &mut Window,
773        _: &mut Context<Self>,
774    ) {
775    }
776
777    fn hide_setting(&self, _: &App) -> Option<workspace::HideStatusItem> {
778        // Activity indicator auto-hides when there's no work to display.
779        None
780    }
781}
782
Served at tenant.openagents/omega Member data and write actions are omitted.