commit a75ae3d63f0060933fb518dba4bdc1d524900055
parent 3cac7164b8a790f3968dfd0f881e9c32795caee7
Author: triesap <tyson@radroots.org>
Date: Tue, 21 Apr 2026 19:44:29 +0000
app: wire pack day host follow-on actions
Diffstat:
7 files changed, 753 insertions(+), 33 deletions(-)
diff --git a/crates/launchers/desktop/src/runtime.rs b/crates/launchers/desktop/src/runtime.rs
@@ -16,7 +16,8 @@ use radroots_app_models::{
FarmId, FarmOrderMethod, FarmProfileRecord, FarmReadiness, FarmRulesProjection, FarmSetupDraft,
FarmSetupProjection, FarmSummary, FarmerSection, FulfillmentWindowId,
LoggedOutStartupProjection, OrderDetailProjection, OrderId, OrderRecoveryProjection,
- OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayProjection,
+ OrdersFilter, OrdersListProjection, OrdersScreenQueryState, PackDayExportBundle,
+ PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayProjection,
PackDayScreenQueryState, PersonalSection, PickupLocationRecord, ProductEditorDraft, ProductId,
ProductsFilter, ProductsListProjection, ProductsSort, RecoveryKind, RecoveryQueueProjection,
RecoveryRecordId, RecoveryState, ReminderDeadlineProjection, ReminderDeliveryState,
@@ -36,8 +37,9 @@ use radroots_app_state::{
AppStateStore, AppStateStoreError, BuyerBrowseScreenProjection, BuyerCartScreenProjection,
BuyerOrdersScreenProjection, BuyerSearchScreenProjection, BuyerSearchScreenQueryState,
FarmSetupFlowStage, FarmWorkspaceReadinessProjection, HomeRoute, OrdersScreenProjection,
- PackDayExportRequest, PackDayScreenProjection, PersistedAppState, PersonalWorkspaceProjection,
- ProductsScreenProjection, ProductsScreenQueryState, derive_sync_projection,
+ PackDayExportRequest, PackDayHostHandoffRequest, PackDayScreenProjection, PersistedAppState,
+ PersonalWorkspaceProjection, ProductsScreenProjection, ProductsScreenQueryState,
+ derive_sync_projection,
};
use radroots_app_sync::{
AppSyncProjection, AppSyncRequest, AppSyncResult, AppSyncTransport, AppSyncTransportError,
@@ -56,6 +58,9 @@ use crate::accounts::{
identity_projection_from_manager, import_local_account, remove_selected_local_key,
reset_local_device_state, select_active_surface, select_local_account,
};
+use crate::pack_day_host_handoff::{
+ PackDayHostHandoffCommandPlan, PackDayHostHandoffError, plan_pack_day_host_handoff,
+};
use crate::remote_signer::{
DesktopRemoteSignerError, DesktopRemoteSignerPaths, activate_pending_session,
apply_remote_signer_custody, clear_pending_session, load_pending_session, purge_all_state,
@@ -409,6 +414,25 @@ impl DesktopAppRuntime {
self.lock_state_mut().export_pack_day()
}
+ pub fn prepare_pack_day_host_handoff(
+ &self,
+ kind: PackDayHostHandoffKind,
+ ) -> Result<
+ Option<(PackDayHostHandoffRequest, PackDayHostHandoffCommandPlan)>,
+ DesktopAppRuntimeCommandError,
+ > {
+ self.lock_state_mut().prepare_pack_day_host_handoff(kind)
+ }
+
+ pub fn finish_pack_day_host_handoff(
+ &self,
+ request: PackDayHostHandoffRequest,
+ result: Result<(), PackDayHostHandoffError>,
+ ) -> Result<bool, DesktopAppRuntimeCommandError> {
+ self.lock_state_mut()
+ .finish_pack_day_host_handoff(request, result)
+ }
+
pub fn update_product_stock(
&self,
product_id: ProductId,
@@ -1791,6 +1815,68 @@ impl DesktopAppRuntimeState {
}
}
+ fn prepare_pack_day_host_handoff(
+ &mut self,
+ kind: PackDayHostHandoffKind,
+ ) -> Result<
+ Option<(PackDayHostHandoffRequest, PackDayHostHandoffCommandPlan)>,
+ DesktopAppRuntimeCommandError,
+ > {
+ if self.state_store.pack_day_projection().host_handoff.status
+ == PackDayHostHandoffStatus::Running
+ {
+ return Ok(None);
+ }
+
+ let Some(bundle) = self.current_pack_day_export_bundle() else {
+ return Ok(None);
+ };
+ let request = PackDayHostHandoffRequest::for_bundle(kind, &bundle);
+ let _ = self
+ .state_store
+ .apply_in_memory(AppStateCommand::begin_pack_day_host_handoff(
+ request.clone(),
+ ));
+
+ match plan_pack_day_host_handoff(&bundle, kind) {
+ Ok(plan) => Ok(Some((request, plan))),
+ Err(error) => {
+ let _ =
+ self.state_store
+ .apply_in_memory(AppStateCommand::fail_pack_day_host_handoff(
+ request,
+ error.to_string(),
+ ));
+ Err(error.into())
+ }
+ }
+ }
+
+ fn finish_pack_day_host_handoff(
+ &mut self,
+ request: PackDayHostHandoffRequest,
+ result: Result<(), PackDayHostHandoffError>,
+ ) -> Result<bool, DesktopAppRuntimeCommandError> {
+ if !self.current_pack_day_host_handoff_request_matches(&request) {
+ return Ok(false);
+ }
+
+ match result {
+ Ok(()) => Ok(self
+ .state_store
+ .apply_in_memory(AppStateCommand::succeed_pack_day_host_handoff(request))),
+ Err(error) => {
+ let _ =
+ self.state_store
+ .apply_in_memory(AppStateCommand::fail_pack_day_host_handoff(
+ request,
+ error.to_string(),
+ ));
+ Err(error.into())
+ }
+ }
+ }
+
fn update_product_stock(
&mut self,
product_id: ProductId,
@@ -3091,6 +3177,26 @@ impl DesktopAppRuntimeState {
let _ = self;
DesktopAppRuntimeCommandError::RuntimeUnavailable
}
+
+ fn current_pack_day_export_bundle(&self) -> Option<PackDayExportBundle> {
+ let pack_day = self.state_store.pack_day_projection();
+ if pack_day.export.status != PackDayExportStatus::Succeeded {
+ return None;
+ }
+
+ let bundle = pack_day.export.bundle.clone()?;
+ let fulfillment_window = pack_day.projection.fulfillment_window.as_ref()?;
+ (fulfillment_window.fulfillment_window_id == bundle.fulfillment_window_id).then_some(bundle)
+ }
+
+ fn current_pack_day_host_handoff_request_matches(
+ &self,
+ request: &PackDayHostHandoffRequest,
+ ) -> bool {
+ let pack_day = self.state_store.pack_day_projection();
+ pack_day.host_handoff.status == PackDayHostHandoffStatus::Running
+ && pack_day.host_handoff.request.as_ref() == Some(request)
+ }
}
#[derive(Debug, Error)]
@@ -3107,6 +3213,8 @@ pub enum DesktopAppRuntimeCommandError {
Sqlite(#[from] AppSqliteError),
#[error(transparent)]
PackDayExportWrite(#[from] PackDayExportWriteError),
+ #[error(transparent)]
+ PackDayHostHandoff(#[from] PackDayHostHandoffError),
}
#[derive(Debug, Error)]
@@ -4437,9 +4545,10 @@ mod tests {
FarmReadinessBlocker, FarmSetupDraft, FarmSetupProjection, FarmSummary,
FarmerActivationProjection, FarmerSection, FulfillmentWindowId, FulfillmentWindowRecord,
LoggedOutStartupProjection, OrderId, OrderStatus, OrdersFilter, PackDayExportStatus,
- PackDayPackListRow, PackDayProductTotalRow, PackDayProjection, PackDayRosterRow,
- PersonalSection, PickupLocationId, PickupLocationRecord, ProductEditorDraft, ProductStatus,
- ProductsFilter, ProductsSort, RecoveryKind, RecoveryRecordId, ReminderDeliveryState,
+ PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow,
+ PackDayProductTotalRow, PackDayProjection, PackDayRosterRow, PersonalSection,
+ PickupLocationId, PickupLocationRecord, ProductEditorDraft, ProductStatus, ProductsFilter,
+ ProductsSort, RecoveryKind, RecoveryRecordId, ReminderDeliveryState,
ReminderFeedProjection, ReminderKind, SelectedSurfaceProjection, SettingsPreference,
SettingsSection, ShellSection, TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
TodaySummary,
@@ -4473,6 +4582,7 @@ mod tests {
DesktopAppSyncStatusSummary, DesktopRemoteSignerPaths, SYNC_TRANSPORT_UNAVAILABLE_MESSAGE,
default_sync_transport,
};
+ use crate::pack_day_host_handoff::PackDayHostHandoffError;
#[derive(Clone)]
struct SharedRecordedSyncTransport(Arc<Mutex<RecordedAppSyncTransport>>);
@@ -7137,6 +7247,138 @@ mod tests {
}
#[test]
+ fn runtime_prepare_pack_day_host_handoff_uses_the_current_export_bundle() {
+ let (runtime, paths) = bootstrapped_runtime("pack_day_host_handoff_prepare");
+ let (_, farm_id) = provision_ready_farmer_account(&runtime);
+
+ seed_order_workspace(&runtime, farm_id);
+ assert!(runtime.open_pack_day(None).expect("pack day should open"));
+ assert!(
+ runtime
+ .export_pack_day()
+ .expect("pack day export should succeed")
+ );
+
+ let prepared = runtime
+ .prepare_pack_day_host_handoff(PackDayHostHandoffKind::OpenPackSheet)
+ .expect("host handoff should prepare")
+ .expect("host handoff should produce a plan");
+
+ let summary = runtime.summary();
+ assert_eq!(
+ summary.pack_day_projection.host_handoff.status,
+ PackDayHostHandoffStatus::Running
+ );
+ assert_eq!(
+ summary.pack_day_projection.host_handoff.request,
+ Some(prepared.0.clone())
+ );
+ assert_eq!(prepared.0.kind, PackDayHostHandoffKind::OpenPackSheet);
+ assert_eq!(
+ prepared.0.bundle_directory,
+ summary
+ .pack_day_projection
+ .export
+ .bundle
+ .as_ref()
+ .expect("pack day export bundle")
+ .bundle_directory
+ );
+ assert_eq!(prepared.1.kind, PackDayHostHandoffKind::OpenPackSheet);
+ assert!(prepared.1.target_path.ends_with("pack_sheet.txt"));
+
+ cleanup_bootstrapped_runtime_paths(&paths);
+ }
+
+ #[test]
+ fn runtime_finish_pack_day_host_handoff_records_failures_in_state() {
+ let (runtime, paths) = bootstrapped_runtime("pack_day_host_handoff_failure");
+ let (_, farm_id) = provision_ready_farmer_account(&runtime);
+
+ seed_order_workspace(&runtime, farm_id);
+ assert!(runtime.open_pack_day(None).expect("pack day should open"));
+ assert!(
+ runtime
+ .export_pack_day()
+ .expect("pack day export should succeed")
+ );
+
+ let (request, _) = runtime
+ .prepare_pack_day_host_handoff(PackDayHostHandoffKind::RevealBundle)
+ .expect("host handoff should prepare")
+ .expect("host handoff should produce a plan");
+
+ let error = runtime
+ .finish_pack_day_host_handoff(
+ request.clone(),
+ Err(PackDayHostHandoffError::UnsupportedPlatform),
+ )
+ .expect_err("host handoff failure should surface");
+ assert!(matches!(
+ error,
+ DesktopAppRuntimeCommandError::PackDayHostHandoff(
+ PackDayHostHandoffError::UnsupportedPlatform
+ )
+ ));
+
+ let summary = runtime.summary();
+ assert_eq!(
+ summary.pack_day_projection.host_handoff.status,
+ PackDayHostHandoffStatus::Failed
+ );
+ assert_eq!(
+ summary.pack_day_projection.host_handoff.request,
+ Some(request)
+ );
+ assert_eq!(
+ summary
+ .pack_day_projection
+ .host_handoff
+ .error_message
+ .as_deref(),
+ Some("pack day host handoff is only supported on macos")
+ );
+
+ cleanup_bootstrapped_runtime_paths(&paths);
+ }
+
+ #[test]
+ fn runtime_finish_pack_day_host_handoff_ignores_stale_background_completion() {
+ let (runtime, paths) = bootstrapped_runtime("pack_day_host_handoff_stale");
+ let (_, farm_id) = provision_ready_farmer_account(&runtime);
+
+ seed_order_workspace(&runtime, farm_id);
+ assert!(runtime.open_pack_day(None).expect("pack day should open"));
+ assert!(
+ runtime
+ .export_pack_day()
+ .expect("pack day export should succeed")
+ );
+
+ let (request, _) = runtime
+ .prepare_pack_day_host_handoff(PackDayHostHandoffKind::RevealBundle)
+ .expect("host handoff should prepare")
+ .expect("host handoff should produce a plan");
+
+ let _ = runtime
+ .lock_state_mut()
+ .state_store
+ .apply_in_memory(AppStateCommand::reset_pack_day_host_handoff());
+
+ assert!(
+ !runtime
+ .finish_pack_day_host_handoff(request, Ok(()))
+ .expect("stale completion should no-op")
+ );
+ assert_eq!(
+ runtime.summary().pack_day_projection.host_handoff.status,
+ PackDayHostHandoffStatus::Idle
+ );
+
+ cleanup_bootstrapped_runtime_paths(&paths);
+ }
+
+ #[test]
fn runtime_threads_canonical_seller_reminders_across_today_orders_and_pack_day() {
let runtime = memory_runtime();
let (_, farm_id) = provision_ready_farmer_account(&runtime);
diff --git a/crates/launchers/desktop/src/window.rs b/crates/launchers/desktop/src/window.rs
@@ -20,14 +20,14 @@ use radroots_app_models::{
FulfillmentWindowId, FulfillmentWindowRecord, FulfillmentWindowSummary, LoggedOutStartupPhase,
OrderDetailItemRow, OrderDetailProjection, OrderId, OrderListRow, OrderPrimaryAction,
OrderRecoveryProjection, OrderStatus, OrdersFilter, OrdersListRow, PackDayExportBundle,
- PackDayExportStatus, PackDayPackListRow, PackDayProductTotalRow, PackDayRosterRow,
- PersonalEntryState, PersonalSection, PickupLocationId, PickupLocationRecord,
- ProductAttentionState, ProductEditorDraft, ProductId, ProductListRow, ProductPricePresentation,
- ProductPublishBlocker, ProductStatus, ProductsFilter, ProductsListRow, ProductsSort,
- RecoveryKind, RecoveryState, ReminderDeadlineProjection, ReminderDeliveryState, ReminderId,
- ReminderLogEntryProjection, ReminderLogProjection, ReminderSurface, ReminderUrgency,
- RepeatDemandEligibility, RepeatDemandHandoffProjection, ShellSection, TodayAgendaProjection,
- TodaySetupTaskKind,
+ PackDayExportStatus, PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayPackListRow,
+ PackDayProductTotalRow, PackDayRosterRow, PersonalEntryState, PersonalSection,
+ PickupLocationId, PickupLocationRecord, ProductAttentionState, ProductEditorDraft, ProductId,
+ ProductListRow, ProductPricePresentation, ProductPublishBlocker, ProductStatus, ProductsFilter,
+ ProductsListRow, ProductsSort, RecoveryKind, RecoveryState, ReminderDeadlineProjection,
+ ReminderDeliveryState, ReminderId, ReminderLogEntryProjection, ReminderLogProjection,
+ ReminderSurface, ReminderUrgency, RepeatDemandEligibility, RepeatDemandHandoffProjection,
+ ShellSection, TodayAgendaProjection, TodaySetupTaskKind,
};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingPollOutcome,
@@ -39,7 +39,7 @@ use radroots_app_remote_signer::{
use radroots_app_sqlite::derive_farm_rules_readiness;
use radroots_app_state::{
FarmSetupFlowStage, FarmWorkspaceStatus, HomeRoute, PackDayExportProjection,
- derive_product_publish_blockers,
+ PackDayHostHandoffRequest, derive_product_publish_blockers,
};
use radroots_app_sync::{
AppSyncRunStatus, SyncAggregateRef, SyncCheckpointState, SyncConflict, SyncConflictKind,
@@ -52,10 +52,10 @@ use radroots_app_ui::{
app_button_icon as action_icon_button, app_button_list_row as list_row_button,
app_button_primary as action_button_primary,
app_button_primary_disabled as action_button_primary_disabled,
- app_button_secondary as action_button, app_button_text as text_button, app_checkbox_field,
- app_cluster, app_detail_row, app_divider as section_divider, app_form_field,
- app_form_input_text, app_form_section, app_heading_section, app_heading_view,
- app_input_text as app_text_input, app_scroll_panel,
+ app_button_secondary as action_button, app_button_secondary_disabled as action_button_disabled,
+ app_button_text as text_button, app_checkbox_field, app_cluster, app_detail_row,
+ app_divider as section_divider, app_form_field, app_form_input_text, app_form_section,
+ app_heading_section, app_heading_view, app_input_text as app_text_input, app_scroll_panel,
app_segment_button_icon as icon_segment_button, app_shared_label_text, app_shared_text,
app_split_shell, app_stack_h, app_stack_v, app_status_indicator as status_indicator,
app_surface_card, app_surface_card_section as home_card, app_surface_panel,
@@ -68,6 +68,9 @@ use radroots_nostr::prelude::RadrootsNostrClient;
use std::{collections::BTreeSet, path::PathBuf, sync::Arc, time::Duration};
use tracing::error;
+use crate::pack_day_host_handoff::{
+ PackDayHostHandoffCommandPlan, PackDayHostHandoffError, execute_pack_day_host_handoff_plan,
+};
use crate::runtime::{
DesktopAppRuntime, DesktopAppRuntimeSummary, DesktopAppSyncConflictSummary,
DesktopAppSyncStatusSummary,
@@ -1644,6 +1647,63 @@ impl HomeView {
}
}
+ fn start_pack_day_host_handoff(
+ &mut self,
+ kind: PackDayHostHandoffKind,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ match self.runtime.prepare_pack_day_host_handoff(kind) {
+ Ok(Some((request, plan))) => {
+ cx.notify();
+ cx.spawn_in(window, async move |this, cx| {
+ let result = cx
+ .background_executor()
+ .spawn(run_pack_day_host_handoff(plan))
+ .await;
+ let _ = this.update(cx, |this, cx| {
+ this.finish_pack_day_host_handoff(request, result, cx);
+ });
+ })
+ .detach();
+ }
+ Ok(None) => {}
+ Err(runtime_error) => {
+ error!(
+ target: "pack_day",
+ event = "pack_day.host_handoff_prepare_failed",
+ kind = %kind.storage_key(),
+ error = %runtime_error,
+ "failed to prepare pack day host handoff"
+ );
+ cx.notify();
+ }
+ }
+ }
+
+ fn finish_pack_day_host_handoff(
+ &mut self,
+ request: PackDayHostHandoffRequest,
+ result: Result<(), PackDayHostHandoffError>,
+ cx: &mut Context<Self>,
+ ) {
+ let kind = request.kind.storage_key();
+ match self.runtime.finish_pack_day_host_handoff(request, result) {
+ Ok(true) => cx.notify(),
+ Ok(false) => {}
+ Err(runtime_error) => {
+ error!(
+ target: "pack_day",
+ event = "pack_day.host_handoff_failed",
+ kind = %kind,
+ error = %runtime_error,
+ "failed to complete pack day host handoff"
+ );
+ cx.notify();
+ }
+ }
+ }
+
fn open_today_next_window(
&mut self,
fulfillment_window_id: Option<FulfillmentWindowId>,
@@ -3376,6 +3436,20 @@ impl HomeView {
.child(pack_day_export_card(
runtime,
cx.listener(|this, _, _, cx| this.export_pack_day(cx)),
+ cx.listener(|this, _, window, cx| {
+ this.start_pack_day_host_handoff(
+ PackDayHostHandoffKind::RevealBundle,
+ window,
+ cx,
+ )
+ }),
+ cx.listener(|this, _, window, cx| {
+ this.start_pack_day_host_handoff(
+ PackDayHostHandoffKind::OpenPackSheet,
+ window,
+ cx,
+ )
+ }),
cx,
))
.when(!projection.reminders.is_empty(), |this| {
@@ -9006,6 +9080,12 @@ async fn run_startup_signer_connect(
.map_err(|error| error.to_string())
}
+async fn run_pack_day_host_handoff(
+ plan: PackDayHostHandoffCommandPlan,
+) -> Result<(), PackDayHostHandoffError> {
+ execute_pack_day_host_handoff_plan(&plan)
+}
+
async fn run_startup_signer_pending_poll(
record: radroots_app_remote_signer::RadrootsAppRemoteSignerSessionRecord,
client_secret_key_hex: String,
@@ -9817,14 +9897,39 @@ struct PackDayExportStatusPresentation {
body_key: AppTextKey,
}
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+struct PackDayHostHandoffActionPresentation {
+ kind: PackDayHostHandoffKind,
+ label_key: AppTextKey,
+ enabled: bool,
+}
+
+#[derive(Clone, Copy, Debug, Eq, PartialEq)]
+struct PackDayHostHandoffStatusPresentation {
+ indicator_color: u32,
+ title_key: AppTextKey,
+}
+
fn pack_day_export_card(
runtime: &DesktopAppRuntimeSummary,
on_export: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ on_reveal_bundle: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
+ on_open_pack_sheet: impl Fn(&ClickEvent, &mut Window, &mut App) + 'static,
cx: &App,
) -> impl IntoElement {
let export = &runtime.pack_day_projection.export;
let status = pack_day_export_status_presentation(runtime);
let detail_rows = pack_day_export_detail_rows(export);
+ let host_handoff_actions = pack_day_host_handoff_action_presentations(runtime);
+ let host_handoff_status = pack_day_host_handoff_status_presentation(runtime);
+ let host_handoff_error_message = runtime
+ .pack_day_projection
+ .host_handoff
+ .error_message
+ .as_deref()
+ .map(str::trim)
+ .filter(|message| !message.is_empty())
+ .map(str::to_owned);
let action = if pack_day_export_action_enabled(runtime) {
action_button_primary(
"pack-day-export",
@@ -9865,10 +9970,202 @@ fn pack_day_export_card(
.when(!detail_rows.is_empty(), |this| {
this.child(label_value_list(detail_rows))
})
- .child(div().child(action)),
+ .child(div().child(action))
+ .when(!host_handoff_actions.is_empty(), |this| {
+ let on_reveal_bundle = Arc::new(on_reveal_bundle);
+ let on_open_pack_sheet = Arc::new(on_open_pack_sheet);
+ this.child(
+ app_stack_v(APP_UI_THEME.foundation.spacing.small_px)
+ .w_full()
+ .child(
+ app_cluster(APP_UI_THEME.foundation.spacing.small_px)
+ .items_center()
+ .children(host_handoff_actions.into_iter().map(move |action| {
+ let button = match action.kind {
+ PackDayHostHandoffKind::RevealBundle if action.enabled => {
+ action_button(
+ "pack-day-reveal-bundle",
+ app_shared_text(action.label_key),
+ {
+ let on_reveal_bundle =
+ Arc::clone(&on_reveal_bundle);
+ move |event, window, cx| {
+ (on_reveal_bundle)(event, window, cx)
+ }
+ },
+ cx,
+ )
+ .into_any_element()
+ }
+ PackDayHostHandoffKind::OpenPackSheet if action.enabled => {
+ action_button(
+ "pack-day-open-pack-sheet",
+ app_shared_text(action.label_key),
+ {
+ let on_open_pack_sheet =
+ Arc::clone(&on_open_pack_sheet);
+ move |event, window, cx| {
+ (on_open_pack_sheet)(event, window, cx)
+ }
+ },
+ cx,
+ )
+ .into_any_element()
+ }
+ PackDayHostHandoffKind::RevealBundle => {
+ action_button_disabled(
+ "pack-day-reveal-bundle",
+ app_shared_text(action.label_key),
+ cx,
+ )
+ .into_any_element()
+ }
+ PackDayHostHandoffKind::OpenPackSheet => {
+ action_button_disabled(
+ "pack-day-open-pack-sheet",
+ app_shared_text(action.label_key),
+ cx,
+ )
+ .into_any_element()
+ }
+ };
+ button
+ })),
+ )
+ .when_some(host_handoff_status, |this, status| {
+ this.child(pack_day_host_handoff_status_note(
+ status,
+ host_handoff_error_message.clone(),
+ ))
+ }),
+ )
+ }),
)
}
+fn pack_day_host_handoff_action_presentations(
+ runtime: &DesktopAppRuntimeSummary,
+) -> Vec<PackDayHostHandoffActionPresentation> {
+ if pack_day_export_succeeded_bundle(runtime).is_none() {
+ return Vec::new();
+ }
+
+ let host_handoff = &runtime.pack_day_projection.host_handoff;
+ let running_kind = (host_handoff.status == PackDayHostHandoffStatus::Running)
+ .then(|| host_handoff.request.as_ref().map(|request| request.kind))
+ .flatten();
+
+ PackDayHostHandoffKind::all_v1()
+ .into_iter()
+ .map(|kind| PackDayHostHandoffActionPresentation {
+ kind,
+ label_key: pack_day_host_handoff_action_label_key(kind, running_kind),
+ enabled: running_kind.is_none(),
+ })
+ .collect()
+}
+
+fn pack_day_host_handoff_action_label_key(
+ kind: PackDayHostHandoffKind,
+ running_kind: Option<PackDayHostHandoffKind>,
+) -> AppTextKey {
+ match (kind, running_kind == Some(kind)) {
+ (PackDayHostHandoffKind::RevealBundle, true) => {
+ AppTextKey::PackDayHostHandoffRevealActionRunning
+ }
+ (PackDayHostHandoffKind::RevealBundle, false) => AppTextKey::PackDayHostHandoffRevealAction,
+ (PackDayHostHandoffKind::OpenPackSheet, true) => {
+ AppTextKey::PackDayHostHandoffOpenPackSheetActionRunning
+ }
+ (PackDayHostHandoffKind::OpenPackSheet, false) => {
+ AppTextKey::PackDayHostHandoffOpenPackSheetAction
+ }
+ }
+}
+
+fn pack_day_host_handoff_status_presentation(
+ runtime: &DesktopAppRuntimeSummary,
+) -> Option<PackDayHostHandoffStatusPresentation> {
+ let host_handoff = &runtime.pack_day_projection.host_handoff;
+ let kind = host_handoff.request.as_ref()?.kind;
+
+ let status = match (host_handoff.status, kind) {
+ (PackDayHostHandoffStatus::Idle, _) => return None,
+ (PackDayHostHandoffStatus::Running, PackDayHostHandoffKind::RevealBundle) => {
+ PackDayHostHandoffStatusPresentation {
+ indicator_color: APP_UI_THEME.foundation.text.accent,
+ title_key: AppTextKey::PackDayHostHandoffRevealRunningTitle,
+ }
+ }
+ (PackDayHostHandoffStatus::Running, PackDayHostHandoffKind::OpenPackSheet) => {
+ PackDayHostHandoffStatusPresentation {
+ indicator_color: APP_UI_THEME.foundation.text.accent,
+ title_key: AppTextKey::PackDayHostHandoffOpenPackSheetRunningTitle,
+ }
+ }
+ (PackDayHostHandoffStatus::Succeeded, PackDayHostHandoffKind::RevealBundle) => {
+ PackDayHostHandoffStatusPresentation {
+ indicator_color: APP_UI_THEME.components.app_status_indicator.online,
+ title_key: AppTextKey::PackDayHostHandoffRevealSucceededTitle,
+ }
+ }
+ (PackDayHostHandoffStatus::Succeeded, PackDayHostHandoffKind::OpenPackSheet) => {
+ PackDayHostHandoffStatusPresentation {
+ indicator_color: APP_UI_THEME.components.app_status_indicator.online,
+ title_key: AppTextKey::PackDayHostHandoffOpenPackSheetSucceededTitle,
+ }
+ }
+ (PackDayHostHandoffStatus::Failed, PackDayHostHandoffKind::RevealBundle) => {
+ PackDayHostHandoffStatusPresentation {
+ indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
+ title_key: AppTextKey::PackDayHostHandoffRevealFailedTitle,
+ }
+ }
+ (PackDayHostHandoffStatus::Failed, PackDayHostHandoffKind::OpenPackSheet) => {
+ PackDayHostHandoffStatusPresentation {
+ indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
+ title_key: AppTextKey::PackDayHostHandoffOpenPackSheetFailedTitle,
+ }
+ }
+ };
+
+ Some(status)
+}
+
+fn pack_day_host_handoff_status_note(
+ status: PackDayHostHandoffStatusPresentation,
+ error_message: Option<String>,
+) -> impl IntoElement {
+ app_stack_v(4.0)
+ .w_full()
+ .child(
+ div()
+ .w_full()
+ .flex()
+ .items_center()
+ .gap(px(APP_UI_THEME.shells.settings_account_status_gap_px))
+ .child(status_indicator(status.indicator_color))
+ .child(
+ div()
+ .text_size(px(APP_UI_THEME.foundation.typography.utility_title_text_px))
+ .font_weight(gpui::FontWeight::SEMIBOLD)
+ .text_color(rgb(APP_UI_THEME.foundation.text.primary))
+ .child(app_shared_text(status.title_key)),
+ ),
+ )
+ .when_some(error_message, |this, error_message| {
+ this.child(home_body_text(error_message))
+ })
+}
+
+fn pack_day_export_succeeded_bundle(
+ runtime: &DesktopAppRuntimeSummary,
+) -> Option<&PackDayExportBundle> {
+ (runtime.pack_day_projection.export.status == PackDayExportStatus::Succeeded)
+ .then_some(runtime.pack_day_projection.export.bundle.as_ref())
+ .flatten()
+}
+
fn pack_day_export_has_exportable_context(runtime: &DesktopAppRuntimeSummary) -> bool {
let projection = &runtime.pack_day_projection.projection;
projection.fulfillment_window.is_some() && !projection.is_empty()
@@ -11804,8 +12101,9 @@ fn home_farm_order_method_label_key(method: FarmOrderMethod) -> AppTextKey {
mod tests {
use super::{
APP_UI_THEME, AppTextKey, FarmerHomeFarmState, HomeAutoFocusState, HomeAutoFocusTarget,
- HomeStage, LabelValueRow, PackDayExportStatusPresentation, ReminderActionTarget,
- SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER,
+ HomeStage, LabelValueRow, PackDayExportStatusPresentation,
+ PackDayHostHandoffActionPresentation, PackDayHostHandoffStatusPresentation,
+ ReminderActionTarget, SETTINGS_FARM_PANEL_SECTIONS, SETTINGS_NAVIGATION_ORDER,
SETTINGS_OPERATIONS_PANEL_SECTIONS, SettingsAutoFocusTarget, SettingsInventorySectionSpec,
SettingsPanelViewKey, StartupHomeSurface, StartupSignerConnectState,
about_conflict_action_specs, about_conflict_aggregate_text, about_conflict_detail_rows,
@@ -11816,6 +12114,7 @@ mod tests {
home_window_launch_size_px, home_window_minimum_size_px, pack_day_export_action_enabled,
pack_day_export_action_label_key, pack_day_export_artifact_names,
pack_day_export_detail_rows, pack_day_export_status_presentation,
+ pack_day_host_handoff_action_presentations, pack_day_host_handoff_status_presentation,
parse_optional_product_editor_stock_input, parse_product_editor_price_input,
presented_farmer_reminder, product_display_title, reminder_action_target,
reminder_deadline_text, reminder_delivery_state_key, reminder_urgency_color,
@@ -11836,10 +12135,11 @@ mod tests {
FulfillmentWindowSummary, LoggedOutStartupPhase, LoggedOutStartupProjection,
OrderDetailProjection, OrderId, OrderPrimaryAction, OrderStatus, OrdersListRow,
PackDayExportArtifact, PackDayExportArtifactKind, PackDayExportBundle,
- PackDayProductTotalRow, PackDayProjection, PersonalSection, ReminderDeadlineProjection,
- ReminderDeliveryState, ReminderId, ReminderKind, ReminderSurface, ReminderUrgency,
- RepeatDemandEligibility, RepeatDemandHandoffProjection, ShellSection,
- TodayAgendaProjection, TodaySetupTask, TodaySetupTaskKind,
+ PackDayHostHandoffKind, PackDayHostHandoffStatus, PackDayProductTotalRow,
+ PackDayProjection, PersonalSection, ReminderDeadlineProjection, ReminderDeliveryState,
+ ReminderId, ReminderKind, ReminderSurface, ReminderUrgency, RepeatDemandEligibility,
+ RepeatDemandHandoffProjection, ShellSection, TodayAgendaProjection, TodaySetupTask,
+ TodaySetupTaskKind,
};
use radroots_app_remote_signer::{
RadrootsAppRemoteSignerApprovedSession, RadrootsAppRemoteSignerPendingSession,
@@ -11847,7 +12147,7 @@ mod tests {
};
use radroots_app_state::{
AppShellProjection, FarmWorkspaceReadinessProjection, FarmWorkspaceStatus, HomeRoute,
- PackDayExportProjection,
+ PackDayExportProjection, PackDayHostHandoffProjection, PackDayHostHandoffRequest,
};
use radroots_app_sync::{
AppSyncProjection, AppSyncRunStatus, SyncAggregateRef, SyncCheckpointStatus, SyncConflict,
@@ -12558,6 +12858,114 @@ mod tests {
}
#[test]
+ fn pack_day_host_handoff_actions_only_surface_after_a_successful_export() {
+ let fulfillment_window_id = FulfillmentWindowId::new();
+ let bundle = PackDayExportBundle {
+ fulfillment_window_id,
+ generated_at_utc: "2026-04-23T15:00:00Z".to_owned(),
+ bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(),
+ artifacts: vec![PackDayExportArtifact {
+ kind: PackDayExportArtifactKind::PackSheet,
+ relative_path: "pack_sheet.txt".to_owned(),
+ }],
+ };
+ let mut runtime = summary(
+ HomeRoute::Today,
+ TodayAgendaProjection::default(),
+ FarmSetupProjection::default(),
+ );
+
+ assert!(pack_day_host_handoff_action_presentations(&runtime).is_empty());
+
+ runtime.pack_day_projection.export = PackDayExportProjection::succeeded(
+ radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id),
+ bundle,
+ );
+
+ assert_eq!(
+ pack_day_host_handoff_action_presentations(&runtime),
+ vec![
+ PackDayHostHandoffActionPresentation {
+ kind: PackDayHostHandoffKind::RevealBundle,
+ label_key: AppTextKey::PackDayHostHandoffRevealAction,
+ enabled: true,
+ },
+ PackDayHostHandoffActionPresentation {
+ kind: PackDayHostHandoffKind::OpenPackSheet,
+ label_key: AppTextKey::PackDayHostHandoffOpenPackSheetAction,
+ enabled: true,
+ },
+ ]
+ );
+ }
+
+ #[test]
+ fn pack_day_host_handoff_running_and_failure_postures_track_the_active_request() {
+ let fulfillment_window_id = FulfillmentWindowId::new();
+ let bundle = PackDayExportBundle {
+ fulfillment_window_id,
+ generated_at_utc: "2026-04-23T15:00:00Z".to_owned(),
+ bundle_directory: "exports/pack_day/window-1/20260423T150000Z".to_owned(),
+ artifacts: vec![PackDayExportArtifact {
+ kind: PackDayExportArtifactKind::PackSheet,
+ relative_path: "pack_sheet.txt".to_owned(),
+ }],
+ };
+ let export_request =
+ radroots_app_state::PackDayExportRequest::for_fulfillment_window(fulfillment_window_id);
+ let reveal_request =
+ PackDayHostHandoffRequest::for_bundle(PackDayHostHandoffKind::RevealBundle, &bundle);
+ let open_request =
+ PackDayHostHandoffRequest::for_bundle(PackDayHostHandoffKind::OpenPackSheet, &bundle);
+ let mut runtime = summary(
+ HomeRoute::Today,
+ TodayAgendaProjection::default(),
+ FarmSetupProjection::default(),
+ );
+ runtime.pack_day_projection.export =
+ PackDayExportProjection::succeeded(export_request, bundle);
+
+ runtime.pack_day_projection.host_handoff =
+ PackDayHostHandoffProjection::running(reveal_request);
+ assert_eq!(
+ pack_day_host_handoff_action_presentations(&runtime),
+ vec![
+ PackDayHostHandoffActionPresentation {
+ kind: PackDayHostHandoffKind::RevealBundle,
+ label_key: AppTextKey::PackDayHostHandoffRevealActionRunning,
+ enabled: false,
+ },
+ PackDayHostHandoffActionPresentation {
+ kind: PackDayHostHandoffKind::OpenPackSheet,
+ label_key: AppTextKey::PackDayHostHandoffOpenPackSheetAction,
+ enabled: false,
+ },
+ ]
+ );
+ assert_eq!(
+ pack_day_host_handoff_status_presentation(&runtime),
+ Some(PackDayHostHandoffStatusPresentation {
+ indicator_color: APP_UI_THEME.foundation.text.accent,
+ title_key: AppTextKey::PackDayHostHandoffRevealRunningTitle,
+ })
+ );
+
+ runtime.pack_day_projection.host_handoff =
+ PackDayHostHandoffProjection::failed(open_request, "finder unavailable");
+ assert_eq!(
+ runtime.pack_day_projection.host_handoff.status,
+ PackDayHostHandoffStatus::Failed
+ );
+ assert_eq!(
+ pack_day_host_handoff_status_presentation(&runtime),
+ Some(PackDayHostHandoffStatusPresentation {
+ indicator_color: APP_UI_THEME.components.app_status_indicator.attention,
+ title_key: AppTextKey::PackDayHostHandoffOpenPackSheetFailedTitle,
+ })
+ );
+ }
+
+ #[test]
fn sidebar_navigation_keeps_the_active_destination_first() {
assert_eq!(
home_sidebar_navigation_sections(FarmerSection::Today, true, false),
diff --git a/crates/shared/i18n/src/keys.rs b/crates/shared/i18n/src/keys.rs
@@ -241,6 +241,16 @@ define_app_text_keys! {
PackDayExportFolderLabel => "pack_day.export.folder.label",
PackDayExportFilesLabel => "pack_day.export.files.label",
PackDayExportErrorLabel => "pack_day.export.error.label",
+ PackDayHostHandoffRevealAction => "pack_day.host_handoff.reveal.action",
+ PackDayHostHandoffRevealActionRunning => "pack_day.host_handoff.reveal.action.running",
+ PackDayHostHandoffOpenPackSheetAction => "pack_day.host_handoff.open_pack_sheet.action",
+ PackDayHostHandoffOpenPackSheetActionRunning => "pack_day.host_handoff.open_pack_sheet.action.running",
+ PackDayHostHandoffRevealRunningTitle => "pack_day.host_handoff.reveal.running.title",
+ PackDayHostHandoffRevealSucceededTitle => "pack_day.host_handoff.reveal.succeeded.title",
+ PackDayHostHandoffRevealFailedTitle => "pack_day.host_handoff.reveal.failed.title",
+ PackDayHostHandoffOpenPackSheetRunningTitle => "pack_day.host_handoff.open_pack_sheet.running.title",
+ PackDayHostHandoffOpenPackSheetSucceededTitle => "pack_day.host_handoff.open_pack_sheet.succeeded.title",
+ PackDayHostHandoffOpenPackSheetFailedTitle => "pack_day.host_handoff.open_pack_sheet.failed.title",
HomeTodayRemindersTitle => "home.today.reminders.title",
ReminderDeadlineLabel => "reminder.deadline.label",
ReminderUrgencyUpcoming => "reminder.urgency.upcoming",
diff --git a/crates/shared/i18n/src/lib.rs b/crates/shared/i18n/src/lib.rs
@@ -475,6 +475,38 @@ mod tests {
assert_eq!(app_text(AppTextKey::PackDayExportFolderLabel), "Folder");
assert_eq!(app_text(AppTextKey::PackDayExportFilesLabel), "Files");
assert_eq!(app_text(AppTextKey::PackDayExportErrorLabel), "Error");
+ assert_eq!(
+ app_text(AppTextKey::PackDayHostHandoffRevealAction),
+ "Show in Finder"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayHostHandoffRevealActionRunning),
+ "Showing in Finder..."
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayHostHandoffOpenPackSheetAction),
+ "Open pack sheet"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayHostHandoffOpenPackSheetActionRunning),
+ "Opening pack sheet..."
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayHostHandoffRevealSucceededTitle),
+ "Shown in Finder"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayHostHandoffOpenPackSheetSucceededTitle),
+ "Opened pack sheet"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayHostHandoffRevealFailedTitle),
+ "Couldn't show in Finder"
+ );
+ assert_eq!(
+ app_text(AppTextKey::PackDayHostHandoffOpenPackSheetFailedTitle),
+ "Couldn't open pack sheet"
+ );
}
#[test]
diff --git a/crates/shared/ui/src/lib.rs b/crates/shared/ui/src/lib.rs
@@ -8,12 +8,13 @@ pub use primitives::{
AppCheckboxFieldSpec, AppFormFieldSpec, AppIconButtonSpec, AppSegmentButtonIconSpec,
LabelValueRow, app_button_card, app_button_choice, app_button_compact, app_button_icon,
app_button_list_row, app_button_primary, app_button_primary_disabled, app_button_secondary,
- app_button_text, app_checkbox_field, app_cluster, app_detail_row, app_divider, app_form_field,
- app_form_input_text, app_form_section, app_heading_section, app_heading_view, app_input_text,
- app_scroll_panel, app_segment_button_icon, app_split_shell, app_stack_h, app_stack_v,
- app_status_indicator, app_surface_card, app_surface_card_section, app_surface_panel,
- app_surface_sidebar, app_surface_window, app_text_badge, app_text_body, app_text_body_subtle,
- app_text_label, app_text_value, label_value_list, utility_title_row,
+ app_button_secondary_disabled, app_button_text, app_checkbox_field, app_cluster,
+ app_detail_row, app_divider, app_form_field, app_form_input_text, app_form_section,
+ app_heading_section, app_heading_view, app_input_text, app_scroll_panel,
+ app_segment_button_icon, app_split_shell, app_stack_h, app_stack_v, app_status_indicator,
+ app_surface_card, app_surface_card_section, app_surface_panel, app_surface_sidebar,
+ app_surface_window, app_text_badge, app_text_body, app_text_body_subtle, app_text_label,
+ app_text_value, label_value_list, utility_title_row,
};
pub use text::{
app_shared_label_text, app_shared_text, runtime_metadata_rows,
diff --git a/crates/shared/ui/src/primitives.rs b/crates/shared/ui/src/primitives.rs
@@ -587,6 +587,23 @@ pub fn app_button_secondary(
)
}
+pub fn app_button_secondary_disabled(
+ id: impl Into<ElementId>,
+ label: impl Into<SharedString>,
+ cx: &App,
+) -> impl IntoElement {
+ app_button_label(
+ app_button_base_disabled(id, AppButtonVariant::Secondary, cx),
+ label.into(),
+ APP_UI_THEME
+ .components
+ .app_button
+ .sizing
+ .horizontal_padding_px,
+ AppButtonVariant::Secondary,
+ )
+}
+
pub fn app_button_primary(
id: impl Into<ElementId>,
label: impl Into<SharedString>,
diff --git a/i18n/locales/en/messages.json b/i18n/locales/en/messages.json
@@ -221,6 +221,16 @@
"pack_day.export.folder.label": "Folder",
"pack_day.export.files.label": "Files",
"pack_day.export.error.label": "Error",
+ "pack_day.host_handoff.reveal.action": "Show in Finder",
+ "pack_day.host_handoff.reveal.action.running": "Showing in Finder...",
+ "pack_day.host_handoff.open_pack_sheet.action": "Open pack sheet",
+ "pack_day.host_handoff.open_pack_sheet.action.running": "Opening pack sheet...",
+ "pack_day.host_handoff.reveal.running.title": "Showing in Finder",
+ "pack_day.host_handoff.reveal.succeeded.title": "Shown in Finder",
+ "pack_day.host_handoff.reveal.failed.title": "Couldn't show in Finder",
+ "pack_day.host_handoff.open_pack_sheet.running.title": "Opening pack sheet",
+ "pack_day.host_handoff.open_pack_sheet.succeeded.title": "Opened pack sheet",
+ "pack_day.host_handoff.open_pack_sheet.failed.title": "Couldn't open pack sheet",
"reminder.deadline.label": "Due",
"reminder.urgency.upcoming": "Upcoming",
"reminder.urgency.due_soon": "Due soon",