feat: full pattern matrix — M1 complete, M2 LLM chat, 30+ pattern files #10
10 changed files with 46 additions and 10 deletions
3
.gitignore
vendored
3
.gitignore
vendored
|
|
@ -29,3 +29,6 @@ src-tauri/target/
|
||||||
# Secrets
|
# Secrets
|
||||||
.env
|
.env
|
||||||
.env.local
|
.env.local
|
||||||
|
|
||||||
|
# Visual companion brainstorm sessions
|
||||||
|
.superpowers/
|
||||||
|
|
|
||||||
|
|
@ -69,3 +69,13 @@ pub fn update_notification_level(level: String, state: State<'_, AppState>) -> R
|
||||||
pub fn get_pending_events() -> Vec<crate::patterns::MatchedEvent> {
|
pub fn get_pending_events() -> Vec<crate::patterns::MatchedEvent> {
|
||||||
crate::notify::take_pending()
|
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);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -102,8 +102,7 @@ impl RobinConfig {
|
||||||
}
|
}
|
||||||
let content = std::fs::read_to_string(&path)
|
let content = std::fs::read_to_string(&path)
|
||||||
.with_context(|| format!("failed to read {}", path.display()))?;
|
.with_context(|| format!("failed to read {}", path.display()))?;
|
||||||
toml::from_str(&content)
|
toml::from_str(&content).with_context(|| format!("failed to parse {}", path.display()))
|
||||||
.with_context(|| format!("failed to parse {}", path.display()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn save(&self) -> Result<()> {
|
pub fn save(&self) -> Result<()> {
|
||||||
|
|
|
||||||
|
|
@ -74,6 +74,8 @@ pub fn run() {
|
||||||
commands::complete_onboarding,
|
commands::complete_onboarding,
|
||||||
commands::update_notification_level,
|
commands::update_notification_level,
|
||||||
commands::get_pending_events,
|
commands::get_pending_events,
|
||||||
|
commands::panel_opened,
|
||||||
|
commands::panel_closed,
|
||||||
])
|
])
|
||||||
.run(tauri::generate_context!())
|
.run(tauri::generate_context!())
|
||||||
.expect("error while running Robin");
|
.expect("error while running Robin");
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use std::sync::atomic::{AtomicBool, Ordering};
|
||||||
use std::sync::Mutex;
|
use std::sync::Mutex;
|
||||||
use tauri::{AppHandle, Emitter, Runtime};
|
use tauri::{AppHandle, Emitter, Runtime};
|
||||||
use tauri_plugin_notification::NotificationExt;
|
use tauri_plugin_notification::NotificationExt;
|
||||||
|
|
@ -7,6 +8,13 @@ use crate::config::NotificationLevel;
|
||||||
use crate::patterns::MatchedEvent;
|
use crate::patterns::MatchedEvent;
|
||||||
|
|
||||||
static PENDING: Mutex<Vec<MatchedEvent>> = Mutex::new(vec![]);
|
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) {
|
pub fn dispatch<R: Runtime>(app: &AppHandle<R>, event: MatchedEvent) {
|
||||||
let level = app
|
let level = app
|
||||||
|
|
@ -16,10 +24,12 @@ pub fn dispatch<R: Runtime>(app: &AppHandle<R>, event: MatchedEvent) {
|
||||||
.map(|c| c.display.notification_level.clone())
|
.map(|c| c.display.notification_level.clone())
|
||||||
.unwrap_or_default();
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
if !PANEL_OPEN.load(Ordering::Relaxed) {
|
||||||
match PENDING.lock() {
|
match PENDING.lock() {
|
||||||
Ok(mut pending) => pending.push(event.clone()),
|
Ok(mut pending) => pending.push(event.clone()),
|
||||||
Err(e) => log::warn!("notify: pending queue lock poisoned — event dropped: {e}"),
|
Err(e) => log::warn!("notify: pending queue lock poisoned — event dropped: {e}"),
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
match level {
|
match level {
|
||||||
NotificationLevel::Off => {}
|
NotificationLevel::Off => {}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{EventSource, SystemEvent, now_unix};
|
use super::{now_unix, EventSource, SystemEvent};
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
use tokio::process::Command;
|
use tokio::process::Command;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
@ -33,6 +33,9 @@ pub async fn watch(tx: mpsc::Sender<SystemEvent>) {
|
||||||
.await;
|
.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> {
|
fn extract_message(line: &str) -> Option<String> {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
use super::{EventSource, SystemEvent, now_unix};
|
use super::{now_unix, EventSource, SystemEvent};
|
||||||
use tokio::fs::File;
|
use tokio::fs::File;
|
||||||
use tokio::io::{AsyncBufReadExt, BufReader};
|
use tokio::io::{AsyncBufReadExt, BufReader};
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
|
|
@ -54,7 +54,10 @@ mod tests {
|
||||||
#[test]
|
#[test]
|
||||||
fn parses_kmsg_line_without_semicolon() {
|
fn parses_kmsg_line_without_semicolon() {
|
||||||
let line = "plain message without header";
|
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]
|
#[test]
|
||||||
|
|
|
||||||
|
|
@ -46,6 +46,11 @@ const messagesEl = ref<HTMLElement | null>(null)
|
||||||
let unlisten: UnlistenFn | null = null
|
let unlisten: UnlistenFn | null = null
|
||||||
|
|
||||||
onMounted(async () => {
|
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 {
|
try {
|
||||||
const pending = await invoke<RobinEvent[]>('get_pending_events')
|
const pending = await invoke<RobinEvent[]>('get_pending_events')
|
||||||
for (const e of pending) {
|
for (const e of pending) {
|
||||||
|
|
@ -63,6 +68,7 @@ onMounted(async () => {
|
||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
unlisten?.()
|
unlisten?.()
|
||||||
|
invoke('panel_closed').catch(() => {})
|
||||||
})
|
})
|
||||||
|
|
||||||
function pushRobinEvent(e: RobinEvent) {
|
function pushRobinEvent(e: RobinEvent) {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue