From f25aa9fdc001111eebbd175fc83f3069b18f3bf1 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Wed, 14 Oct 2026 08:47:07 +0700 Subject: [PATCH] Track per-step push progress and show it in a banner --- crates/signed_state/src/repo.rs | 28 +++++++++++- crates/signed_state/src/state.rs | 22 ++++++++++ crates/workspace/src/views/repo/mod.rs | 59 +++++++++++++++++++++++++- 3 files changed, 107 insertions(+), 2 deletions(-) diff --git a/crates/signed_state/src/repo.rs b/crates/signed_state/src/repo.rs index 5edcaae..a1b29d6 100644 --- a/crates/signed_state/src/repo.rs +++ b/crates/signed_state/src/repo.rs @@ -18,7 +18,7 @@ use crate::checkouts::CheckoutsStore; use crate::patch_series::{MAX_PATCH_EVENT_BYTES, PatchSeries}; use crate::push::{PushOutcome, grasp_base_url, grasp06_prs_url, pr_clone_urls}; use crate::repos::RepoListStore; -use crate::state::{GitRef, SyncState}; +use crate::state::{GitRef, PushStepStatus, SyncState}; use crate::sync_status::SyncStatusStore; pub struct RepoStore { @@ -48,6 +48,7 @@ pub struct RepoStore { // The repository is out of sync on the rejected servers until republished. pub last_push_warning: Option, pub pushing: bool, + pub push_steps: Vec, pub cloning: bool, // Avoids re-subscribing and re-fetching on every refresh. repo_relays: HashSet, @@ -101,6 +102,7 @@ impl RepoStore { last_warning: None, last_push_warning: None, pushing: false, + push_steps: Vec::new(), cloning: false, repo_relays: HashSet::new(), root_fetches: HashSet::new(), @@ -131,6 +133,7 @@ impl RepoStore { last_warning: None, last_push_warning: None, pushing: false, + push_steps: Vec::new(), cloning: false, repo_relays: HashSet::new(), root_fetches: HashSet::new(), @@ -169,6 +172,14 @@ impl RepoStore { let backend = Backend::global(cx); cx.subscribe(&backend, |this, _backend, event, cx| { + if let BackendEvent::PushProgress { addr, status } = event { + if this.addr.as_ref() == Some(addr) { + this.apply_push_step(status.clone(), cx); + } + + return; + } + let Some(addr) = this.addr.as_ref() else { return; }; @@ -203,6 +214,19 @@ impl RepoStore { self.addr.as_ref() } + fn apply_push_step(&mut self, status: PushStepStatus, cx: &mut Context) { + match self + .push_steps + .iter_mut() + .find(|entry| entry.step() == status.step()) + { + Some(entry) => *entry = status, + None => self.push_steps.push(status), + } + + cx.notify(); + } + pub fn name(&self) -> SharedString { self.announcement .as_ref() @@ -1186,6 +1210,7 @@ impl RepoStore { cx: &mut Context, ) -> Task> { self.pushing = true; + self.push_steps.clear(); self.last_error = None; self.last_push_warning = None; cx.notify(); @@ -1195,6 +1220,7 @@ impl RepoStore { this.update(cx, |this, cx| { this.pushing = false; + this.push_steps.clear(); match &result { Ok(outcome) => { diff --git a/crates/signed_state/src/state.rs b/crates/signed_state/src/state.rs index a8102f7..901651f 100644 --- a/crates/signed_state/src/state.rs +++ b/crates/signed_state/src/state.rs @@ -67,6 +67,28 @@ pub enum PushStepStatus { Failed(PushStep, String), } +impl PushStep { + pub fn label(&self) -> String { + match self { + Self::AnnounceRepository => "Announce repository".to_owned(), + Self::StageStateEvent(relay) => format!("Publish state event -> {relay}"), + Self::BroadcastStateEvent => "Publish state event to relays".to_owned(), + Self::PushObjects(relay) => format!( + "Push objects -> {}", + grasp_base_url(relay).unwrap_or_else(|| relay.to_string()) + ), + } + } +} + +impl PushStepStatus { + pub fn step(&self) -> &PushStep { + match self { + Self::Running(step) | Self::Done(step) | Self::Failed(step, _) => step, + } + } +} + impl SyncState { pub fn build(id: &str, refs: &[GitRef], head: Option<&str>) -> EventBuilder { let mut tags: Vec = vec![Tag::identifier(id.to_owned())]; diff --git a/crates/workspace/src/views/repo/mod.rs b/crates/workspace/src/views/repo/mod.rs index c750ed5..463fd87 100644 --- a/crates/workspace/src/views/repo/mod.rs +++ b/crates/workspace/src/views/repo/mod.rs @@ -26,7 +26,8 @@ use signed_core::{Announcement, RepoAddr, RepoStatus}; use signed_git::{FileCommit, GitCache, RefSync, Repo}; use signed_state::{ Backend, CheckoutStatus, CheckoutSyncStatus, CheckoutsStore, LocalReposStore, Mirrors, - Nip34Binding, Nip34Kind, ProfileStore, RepoListStore, RepoStore, SyncStatusStore, + Nip34Binding, Nip34Kind, ProfileStore, PushStepStatus, RepoListStore, RepoStore, + SyncStatusStore, }; use signed_ui::{ Avatar, CountBadge, DropdownButton, PixelAvatar, copy_row, menu_copy_row, ref_selector_trigger, @@ -1747,6 +1748,59 @@ impl RepoDetailView { ) } + pub(super) fn render_push_progress_banner(&self, cx: &Context) -> Option { + let store = self.store.read(cx); + if !store.pushing || store.push_steps.is_empty() { + return None; + } + + let steps = store + .push_steps + .iter() + .map(|status| { + let label = SharedString::from(status.step().label()); + + match status { + PushStepStatus::Running(_) => h_flex() + .gap_2() + .text_sm() + .text_color(cx.theme().muted_foreground) + .child(Spinner::new().small()) + .child(label) + .into_any_element(), + PushStepStatus::Done(_) => h_flex() + .gap_2() + .text_sm() + .text_color(cx.theme().foreground) + .child(Icon::new(IconName::CircleCheck).small()) + .child(label) + .into_any_element(), + PushStepStatus::Failed(_, reason) => h_flex() + .gap_2() + .text_sm() + .text_color(cx.theme().danger) + .child(Icon::new(IconName::Close).small()) + .child(SharedString::from(format!( + "{}: {reason}", + status.step().label() + ))) + .into_any_element(), + } + }) + .collect::>(); + + Some( + v_flex() + .p_4() + .gap_1() + .w_full() + .bg(cx.theme().muted) + .child(div().text_sm().child("Pushing")) + .children(steps) + .into_any_element(), + ) + } + pub(super) fn render_push_warning_banner(&self, cx: &Context) -> Option { let store = self.store.read(cx); let warning = store.last_push_warning.clone()?; @@ -2189,6 +2243,9 @@ impl Render for RepoDetailView { .id("repo") .size_full() .when_some(banner, |this, banner| this.child(banner)) + .when_some(self.render_push_progress_banner(cx), |this, banner| { + this.child(banner) + }) .when_some(self.render_push_warning_banner(cx), |this, banner| { this.child(banner) })