Skip to content

Commit 77ad959

Browse files
committed
feat: add batch operations, operation queue, toast notifications, and per-package progress
- Batch install/remove/update: select multiple packages via checkboxes and operate on them together with floating action bars - Operation queue: if an operation is already running, new ones are queued and processed sequentially - Toast notifications: success/error toasts with auto-dismiss - Per-package progress: route soar events to per-package status map so each card shows its own live progress (download %, verifying, etc.) with inline progress bars during downloads - Quiet reload on remove: optimistically remove from local list and reload without the "Loading..." flash - Confirm dialog improvements: batch confirm with package list display
1 parent ab6c09a commit 77ad959

File tree

10 files changed

+1330
-161
lines changed

10 files changed

+1330
-161
lines changed

src/adapters/soar.rs

Lines changed: 136 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -753,11 +753,87 @@ impl Adapter for SoarAdapter {
753753

754754
async fn install(
755755
&self,
756-
_packages: &[Package],
756+
packages: &[Package],
757757
_progress: Option<ProgressSender>,
758-
_mode: PackageMode,
758+
mode: PackageMode,
759759
) -> Result<Vec<InstallResult>> {
760-
Err(AdapterError::NotSupported)
760+
if mode == PackageMode::System {
761+
let settings = HashMap::new();
762+
for pkg in packages {
763+
let query = pkg.soar_query().ok_or_else(|| {
764+
AdapterError::Other(format!("Invalid package id: {}", pkg.id))
765+
})?;
766+
self.install_system_package(&query, &settings).await?;
767+
}
768+
return Ok(packages
769+
.iter()
770+
.map(|p| InstallResult {
771+
package_name: p.name.clone(),
772+
package_id: p.id.clone(),
773+
version: p.version.clone(),
774+
success: true,
775+
error: None,
776+
})
777+
.collect());
778+
}
779+
780+
let queries: Vec<String> = packages
781+
.iter()
782+
.map(|p| {
783+
p.soar_query()
784+
.ok_or_else(|| AdapterError::Other(format!("Invalid package id: {}", p.id)))
785+
})
786+
.collect::<Result<_>>()?;
787+
788+
let ctx = self.user_ctx();
789+
let options = InstallOptions::default();
790+
let results = install::resolve_packages(&ctx, &queries, &options)
791+
.await
792+
.map_err(|e| AdapterError::Other(e.to_string()))?;
793+
794+
let mut targets = Vec::new();
795+
for result in results {
796+
match result {
797+
ResolveResult::Resolved(t) => targets.extend(t),
798+
ResolveResult::AlreadyInstalled { pkg_name, .. } => {
799+
return Err(AdapterError::Other(format!(
800+
"{pkg_name} is already installed"
801+
)));
802+
}
803+
ResolveResult::NotFound(q) => {
804+
return Err(AdapterError::Other(format!("Package not found: {q}")));
805+
}
806+
ResolveResult::Ambiguous(amb) => {
807+
return Err(AdapterError::Other(format!(
808+
"Ambiguous package query: {}",
809+
amb.query
810+
)));
811+
}
812+
}
813+
}
814+
815+
if targets.is_empty() {
816+
return Err(AdapterError::Other("No packages to install".into()));
817+
}
818+
819+
let report = install::perform_installation(&ctx, targets, &options)
820+
.await
821+
.map_err(|e| AdapterError::Other(e.to_string()))?;
822+
823+
if let Some(failed) = report.failed.first() {
824+
return Err(AdapterError::Other(failed.error.clone()));
825+
}
826+
827+
Ok(packages
828+
.iter()
829+
.map(|p| InstallResult {
830+
package_name: p.name.clone(),
831+
package_id: p.id.clone(),
832+
version: p.version.clone(),
833+
success: true,
834+
error: None,
835+
})
836+
.collect())
761837
}
762838

763839
async fn remove(
@@ -805,11 +881,65 @@ impl Adapter for SoarAdapter {
805881

806882
async fn update(
807883
&self,
808-
_packages: &[Package],
884+
packages: &[Package],
809885
_progress: Option<ProgressSender>,
810-
_mode: PackageMode,
886+
mode: PackageMode,
811887
) -> Result<Vec<InstallResult>> {
812-
Err(AdapterError::NotSupported)
888+
if mode == PackageMode::System {
889+
let settings = HashMap::new();
890+
for pkg in packages {
891+
let query = pkg.soar_query().ok_or_else(|| {
892+
AdapterError::Other(format!("Invalid package id: {}", pkg.id))
893+
})?;
894+
self.update_system_package(&query, &settings).await?;
895+
}
896+
return Ok(packages
897+
.iter()
898+
.map(|p| InstallResult {
899+
package_name: p.name.clone(),
900+
package_id: p.id.clone(),
901+
version: p.version.clone(),
902+
success: true,
903+
error: None,
904+
})
905+
.collect());
906+
}
907+
908+
let queries: Vec<String> = packages
909+
.iter()
910+
.map(|p| {
911+
p.soar_query()
912+
.ok_or_else(|| AdapterError::Other(format!("Invalid package id: {}", p.id)))
913+
})
914+
.collect::<Result<_>>()?;
915+
916+
let ctx = self.user_ctx();
917+
let updates = update::check_updates(&ctx, Some(&queries))
918+
.await
919+
.map_err(|e| AdapterError::Other(e.to_string()))?;
920+
921+
if updates.is_empty() {
922+
return Err(AdapterError::Other("No updates available".into()));
923+
}
924+
925+
let report = update::perform_update(&ctx, updates, false, false)
926+
.await
927+
.map_err(|e| AdapterError::Other(e.to_string()))?;
928+
929+
if let Some(failed) = report.failed.first() {
930+
return Err(AdapterError::Other(failed.error.clone()));
931+
}
932+
933+
Ok(packages
934+
.iter()
935+
.map(|p| InstallResult {
936+
package_name: p.name.clone(),
937+
package_id: p.id.clone(),
938+
version: p.version.clone(),
939+
success: true,
940+
error: None,
941+
})
942+
.collect())
813943
}
814944

815945
async fn list_installed(&self, mode: PackageMode) -> Result<Vec<InstalledPackage>> {

src/adapters/wasm/adapter.rs

Lines changed: 55 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,51 @@ impl WasmAdapter {
248248
let linker = self.linker.clone();
249249
let host_state = self.host_state.clone();
250250

251+
tokio::task::spawn_blocking(move || {
252+
let (mut store, instance) =
253+
Self::fresh_instance(&engine, &module, &linker, host_state)?;
254+
255+
let input_json = serde_json::to_string(&input).map_err(|e| {
256+
AdapterError::PluginError(format!("Failed to serialize input: {e}"))
257+
})?;
258+
259+
let (ptr, len) = memory::write_string(&instance, &mut store, &input_json)
260+
.map_err(AdapterError::PluginError)?;
261+
262+
let func = instance
263+
.get_typed_func::<(i32, i32), i64>(store.as_context_mut(), export_name)
264+
.map_err(|e| {
265+
AdapterError::PluginError(format!("Missing export '{export_name}': {e}"))
266+
})?;
267+
268+
let result = func
269+
.call(&mut store, (ptr as i32, len as i32))
270+
.map_err(|e| AdapterError::PluginError(format!("{export_name} failed: {e}")))?;
271+
272+
let mem = memory::get_memory(&instance, store.as_context_mut())
273+
.map_err(AdapterError::PluginError)?;
274+
275+
memory::read_result_json(&mem, &store, result).map_err(AdapterError::PluginError)
276+
})
277+
.await
278+
.map_err(|e| AdapterError::Other(format!("Task join error: {e}")))?
279+
}
280+
/// Like call_with_json but sets progress_sender on the HostState.
281+
async fn call_with_json_progress<
282+
I: serde::Serialize + Send + 'static,
283+
O: serde::de::DeserializeOwned + Send + 'static,
284+
>(
285+
&self,
286+
export_name: &'static str,
287+
input: I,
288+
progress: Option<ProgressSender>,
289+
) -> Result<O> {
290+
let engine = self.engine.clone();
291+
let module = self.module.clone();
292+
let linker = self.linker.clone();
293+
let mut host_state = self.host_state.clone();
294+
host_state.progress_sender = progress;
295+
251296
tokio::task::spawn_blocking(move || {
252297
let (mut store, instance) =
253298
Self::fresh_instance(&engine, &module, &linker, host_state)?;
@@ -318,41 +363,45 @@ impl Adapter for WasmAdapter {
318363
async fn install(
319364
&self,
320365
packages: &[Package],
321-
_progress: Option<ProgressSender>,
366+
progress: Option<ProgressSender>,
322367
mode: PackageMode,
323368
) -> Result<Vec<InstallResult>> {
324369
let input = PackagesWithMode {
325370
packages: packages.to_vec(),
326371
mode: mode_str(mode),
327372
};
328-
self.call_with_json(abi::EXPORT_INSTALL, input).await
373+
self.call_with_json_progress(abi::EXPORT_INSTALL, input, progress)
374+
.await
329375
}
330376

331377
async fn remove(
332378
&self,
333379
packages: &[Package],
334-
_progress: Option<ProgressSender>,
380+
progress: Option<ProgressSender>,
335381
mode: PackageMode,
336382
) -> Result<()> {
337383
let input = PackagesWithMode {
338384
packages: packages.to_vec(),
339385
mode: mode_str(mode),
340386
};
341-
let _: serde_json::Value = self.call_with_json(abi::EXPORT_REMOVE, input).await?;
387+
let _: serde_json::Value = self
388+
.call_with_json_progress(abi::EXPORT_REMOVE, input, progress)
389+
.await?;
342390
Ok(())
343391
}
344392

345393
async fn update(
346394
&self,
347395
packages: &[Package],
348-
_progress: Option<ProgressSender>,
396+
progress: Option<ProgressSender>,
349397
mode: PackageMode,
350398
) -> Result<Vec<InstallResult>> {
351399
let input = PackagesWithMode {
352400
packages: packages.to_vec(),
353401
mode: mode_str(mode),
354402
};
355-
self.call_with_json(abi::EXPORT_UPDATE, input).await
403+
self.call_with_json_progress(abi::EXPORT_UPDATE, input, progress)
404+
.await
356405
}
357406

358407
async fn list_installed(&self, mode: PackageMode) -> Result<Vec<InstalledPackage>> {

src/adapters/wasm/host.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
use std::path::{Path, PathBuf};
22

3+
use crate::core::adapter::ProgressSender;
4+
35
use super::manifest::Permissions;
46

57
#[derive(Clone)]
68
pub struct HostState {
79
pub adapter_id: String,
810
pub permissions: Permissions,
911
pub data_dir: PathBuf,
12+
pub progress_sender: Option<ProgressSender>,
1013
}
1114

1215
impl HostState {
@@ -16,6 +19,7 @@ impl HostState {
1619
adapter_id,
1720
permissions,
1821
data_dir,
22+
progress_sender: None,
1923
}
2024
}
2125

src/adapters/wasm/host_functions.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -407,9 +407,16 @@ fn register_host_report_progress(linker: &mut Linker<HostState>) -> Result<(), S
407407
Err(_) => return,
408408
};
409409

410-
// Log the progress event; actual channel forwarding is a follow-up
411410
let adapter_id = caller.data().adapter_id.clone();
412411
log::debug!("[plugin:{adapter_id}] progress: {json_str}");
412+
413+
// Forward progress event to sender if available
414+
if let Some(ref sender) = caller.data().progress_sender {
415+
let _ = sender.send(crate::core::adapter::ProgressEvent::Status {
416+
adapter_id,
417+
message: json_str,
418+
});
419+
}
413420
},
414421
)
415422
.map_err(|e| format!("Failed to register {}: {e}", abi::IMPORT_REPORT_PROGRESS))?;

0 commit comments

Comments
 (0)