From d8991905d75565da31655d2287b21f2b849c172d Mon Sep 17 00:00:00 2001 From: pyr0ball Date: Mon, 18 May 2026 18:56:45 -0700 Subject: [PATCH] =?UTF-8?q?fix(m1):=20address=20HIGH=20review=20findings?= =?UTF-8?q?=20=E2=80=94=20journald=20zombie,=20silent=20exit,=20double-del?= =?UTF-8?q?ivery?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 3 +++ src-tauri/build.rs | 2 +- src-tauri/src/commands.rs | 10 ++++++++++ src-tauri/src/config.rs | 3 +-- src-tauri/src/lib.rs | 2 ++ src-tauri/src/main.rs | 2 +- src-tauri/src/notify.rs | 16 +++++++++++++--- src-tauri/src/watcher/journald.rs | 5 ++++- src-tauri/src/watcher/kmsg.rs | 7 +++++-- src/components/ChatPanel.vue | 6 ++++++ 10 files changed, 46 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index a37692b..b2f91ef 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ src-tauri/target/ # Secrets .env .env.local + +# Visual companion brainstorm sessions +.superpowers/ diff --git a/src-tauri/build.rs b/src-tauri/build.rs index 795b9b7..d860e1e 100644 --- a/src-tauri/build.rs +++ b/src-tauri/build.rs @@ -1,3 +1,3 @@ fn main() { - tauri_build::build() + tauri_build::build() } diff --git a/src-tauri/src/commands.rs b/src-tauri/src/commands.rs index 5900532..cf96e71 100644 --- a/src-tauri/src/commands.rs +++ b/src-tauri/src/commands.rs @@ -69,3 +69,13 @@ pub fn update_notification_level(level: String, state: State<'_, AppState>) -> R pub fn get_pending_events() -> Vec { crate::notify::take_pending() } + +#[tauri::command] +pub fn panel_opened() { + crate::notify::set_panel_open(true); +} + +#[tauri::command] +pub fn panel_closed() { + crate::notify::set_panel_open(false); +} diff --git a/src-tauri/src/config.rs b/src-tauri/src/config.rs index f2c209d..0197af2 100644 --- a/src-tauri/src/config.rs +++ b/src-tauri/src/config.rs @@ -102,8 +102,7 @@ impl RobinConfig { } let content = std::fs::read_to_string(&path) .with_context(|| format!("failed to read {}", path.display()))?; - toml::from_str(&content) - .with_context(|| format!("failed to parse {}", path.display())) + toml::from_str(&content).with_context(|| format!("failed to parse {}", path.display())) } pub fn save(&self) -> Result<()> { diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index b8f4429..5da7576 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -74,6 +74,8 @@ pub fn run() { commands::complete_onboarding, commands::update_notification_level, commands::get_pending_events, + commands::panel_opened, + commands::panel_closed, ]) .run(tauri::generate_context!()) .expect("error while running Robin"); diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index ad5fe83..69c3a72 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -2,5 +2,5 @@ #![cfg_attr(not(debug_assertions), windows_subsystem = "windows")] fn main() { - app_lib::run(); + app_lib::run(); } diff --git a/src-tauri/src/notify.rs b/src-tauri/src/notify.rs index 1cfc57a..7dc9637 100644 --- a/src-tauri/src/notify.rs +++ b/src-tauri/src/notify.rs @@ -1,3 +1,4 @@ +use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::Mutex; use tauri::{AppHandle, Emitter, Runtime}; use tauri_plugin_notification::NotificationExt; @@ -7,6 +8,13 @@ use crate::config::NotificationLevel; use crate::patterns::MatchedEvent; static PENDING: Mutex> = Mutex::new(vec![]); +// True while ChatPanel is mounted and listening for live events. +// When true, dispatch skips PENDING so events are not shown twice on re-open. +static PANEL_OPEN: AtomicBool = AtomicBool::new(false); + +pub fn set_panel_open(open: bool) { + PANEL_OPEN.store(open, Ordering::Relaxed); +} pub fn dispatch(app: &AppHandle, event: MatchedEvent) { let level = app @@ -16,9 +24,11 @@ pub fn dispatch(app: &AppHandle, event: MatchedEvent) { .map(|c| c.display.notification_level.clone()) .unwrap_or_default(); - match PENDING.lock() { - Ok(mut pending) => pending.push(event.clone()), - Err(e) => log::warn!("notify: pending queue lock poisoned — event dropped: {e}"), + if !PANEL_OPEN.load(Ordering::Relaxed) { + match PENDING.lock() { + Ok(mut pending) => pending.push(event.clone()), + Err(e) => log::warn!("notify: pending queue lock poisoned — event dropped: {e}"), + } } match level { diff --git a/src-tauri/src/watcher/journald.rs b/src-tauri/src/watcher/journald.rs index c1c3564..3c170a6 100644 --- a/src-tauri/src/watcher/journald.rs +++ b/src-tauri/src/watcher/journald.rs @@ -1,4 +1,4 @@ -use super::{EventSource, SystemEvent, now_unix}; +use super::{now_unix, EventSource, SystemEvent}; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::process::Command; use tokio::sync::mpsc; @@ -33,6 +33,9 @@ pub async fn watch(tx: mpsc::Sender) { .await; } } + + log::warn!("journald watcher: journalctl exited — no new journal events will be observed"); + let _ = child.wait().await; } fn extract_message(line: &str) -> Option { diff --git a/src-tauri/src/watcher/kmsg.rs b/src-tauri/src/watcher/kmsg.rs index f3c524c..7dd7582 100644 --- a/src-tauri/src/watcher/kmsg.rs +++ b/src-tauri/src/watcher/kmsg.rs @@ -1,4 +1,4 @@ -use super::{EventSource, SystemEvent, now_unix}; +use super::{now_unix, EventSource, SystemEvent}; use tokio::fs::File; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::sync::mpsc; @@ -54,7 +54,10 @@ mod tests { #[test] fn parses_kmsg_line_without_semicolon() { let line = "plain message without header"; - assert_eq!(parse_kmsg(line), Some("plain message without header".to_string())); + assert_eq!( + parse_kmsg(line), + Some("plain message without header".to_string()) + ); } #[test] diff --git a/src/components/ChatPanel.vue b/src/components/ChatPanel.vue index 3ec94b7..193c636 100644 --- a/src/components/ChatPanel.vue +++ b/src/components/ChatPanel.vue @@ -46,6 +46,11 @@ const messagesEl = ref(null) let unlisten: UnlistenFn | null = null onMounted(async () => { + // Signal backend: stop queuing events into PENDING while panel is open. + // Must happen before drain so new events that arrive during mount are not + // double-delivered on the next open cycle. + try { await invoke('panel_opened') } catch {} + try { const pending = await invoke('get_pending_events') for (const e of pending) { @@ -63,6 +68,7 @@ onMounted(async () => { onUnmounted(() => { unlisten?.() + invoke('panel_closed').catch(() => {}) }) function pushRobinEvent(e: RobinEvent) {