feat: full pattern matrix — M1 complete, M2 LLM chat, 30+ pattern files #10

Open
pyr0ball wants to merge 31 commits from feat/patterns-expansion into main
10 changed files with 46 additions and 10 deletions
Showing only changes of commit d8991905d7 - Show all commits

3
.gitignore vendored
View file

@ -29,3 +29,6 @@ src-tauri/target/
# Secrets
.env
.env.local
# Visual companion brainstorm sessions
.superpowers/

View file

@ -1,3 +1,3 @@
fn main() {
tauri_build::build()
tauri_build::build()
}

View file

@ -69,3 +69,13 @@ pub fn update_notification_level(level: String, state: State<'_, AppState>) -> R
pub fn get_pending_events() -> Vec<crate::patterns::MatchedEvent> {
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);
}

View file

@ -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<()> {

View file

@ -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");

View file

@ -2,5 +2,5 @@
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
fn main() {
app_lib::run();
app_lib::run();
}

View file

@ -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<Vec<MatchedEvent>> = 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<R: Runtime>(app: &AppHandle<R>, event: MatchedEvent) {
let level = app
@ -16,9 +24,11 @@ pub fn dispatch<R: Runtime>(app: &AppHandle<R>, 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 {

View file

@ -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<SystemEvent>) {
.await;
}
}
log::warn!("journald watcher: journalctl exited — no new journal events will be observed");
let _ = child.wait().await;
}
fn extract_message(line: &str) -> Option<String> {

View file

@ -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]

View file

@ -46,6 +46,11 @@ const messagesEl = ref<HTMLElement | null>(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<RobinEvent[]>('get_pending_events')
for (const e of pending) {
@ -63,6 +68,7 @@ onMounted(async () => {
onUnmounted(() => {
unlisten?.()
invoke('panel_closed').catch(() => {})
})
function pushRobinEvent(e: RobinEvent) {