diff --git a/src/extensions/manager.rs b/src/extensions/manager.rs index cdcb459a..44d598cd 100644 --- a/src/extensions/manager.rs +++ b/src/extensions/manager.rs @@ -168,6 +168,53 @@ impl ExtensionManager { active.extend(names); } + /// Persist the set of active channel names to the settings store. + /// + /// Saved under key `activated_channels` so channels auto-activate on restart. + async fn persist_active_channels(&self) { + let Some(ref store) = self.store else { + return; + }; + let names: Vec = self + .active_channel_names + .read() + .await + .iter() + .cloned() + .collect(); + let value = serde_json::json!(names); + if let Err(e) = store + .set_setting(&self.user_id, "activated_channels", &value) + .await + { + tracing::warn!(error = %e, "Failed to persist activated_channels setting"); + } + } + + /// Load previously activated channel names from the settings store. + /// + /// Returns channel names that were activated in a prior session so they can + /// be auto-activated at startup. + pub async fn load_persisted_active_channels(&self) -> Vec { + let Some(ref store) = self.store else { + return Vec::new(); + }; + match store.get_setting(&self.user_id, "activated_channels").await { + Ok(Some(value)) => match serde_json::from_value(value) { + Ok(names) => names, + Err(e) => { + tracing::warn!(error = %e, "Failed to deserialize activated_channels"); + Vec::new() + } + }, + Ok(None) => Vec::new(), + Err(e) => { + tracing::warn!(error = %e, "Failed to load activated_channels setting"); + Vec::new() + } + } + } + /// Set the SSE broadcast sender for pushing extension status events to the web UI. pub async fn set_sse_sender( &self, @@ -530,6 +577,10 @@ impl ExtensionManager { Ok(format!("Removed WASM tool '{}'", name)) } ExtensionKind::WasmChannel => { + // Remove from active set and persist + self.active_channel_names.write().await.remove(name); + self.persist_active_channels().await; + // Delete channel files let wasm_path = self.wasm_channels_dir.join(format!("{}.wasm", name)); let cap_path = self @@ -1993,6 +2044,9 @@ impl ExtensionManager { .await .insert(channel_name.clone()); + // Persist activation state so the channel auto-activates on restart + self.persist_active_channels().await; + tracing::info!(channel = %channel_name, "Hot-activated WASM channel"); Ok(ActivateResult { diff --git a/src/main.rs b/src/main.rs index 55a56b1d..08728307 100644 --- a/src/main.rs +++ b/src/main.rs @@ -609,6 +609,8 @@ async fn async_main() -> anyhow::Result<()> { if let Some(ref ext_mgr) = components.extension_manager && let Some((rt, ps, router)) = wasm_channel_runtime_state.take() { + let active_at_startup: std::collections::HashSet = + loaded_wasm_channel_names.iter().cloned().collect(); ext_mgr.set_active_channels(loaded_wasm_channel_names).await; ext_mgr .set_channel_runtime( @@ -620,6 +622,29 @@ async fn async_main() -> anyhow::Result<()> { ) .await; tracing::info!("Channel runtime wired into extension manager for hot-activation"); + + // Auto-activate channels that were active in a previous session. + let persisted = ext_mgr.load_persisted_active_channels().await; + for name in &persisted { + if !active_at_startup.contains(name) { + match ext_mgr.activate(name).await { + Ok(result) => { + tracing::info!( + channel = %name, + message = %result.message, + "Auto-activated persisted channel" + ); + } + Err(e) => { + tracing::warn!( + channel = %name, + error = %e, + "Failed to auto-activate persisted channel" + ); + } + } + } + } } // Wire SSE sender into extension manager for broadcasting status events.