diff --git a/app/src-tauri/src/bridge.rs b/app/src-tauri/src/bridge.rs index 4e3c592..d16e315 100644 --- a/app/src-tauri/src/bridge.rs +++ b/app/src-tauri/src/bridge.rs @@ -430,6 +430,11 @@ pub async fn mark_read(state: BridgeState<'_>, target: Value) -> Result) -> Result { + data_of(state.request(Cmd::ClearEvents).await.map_err(|e| e.to_string())?) +} + #[tauri::command] pub async fn health(state: BridgeState<'_>) -> Result { data_of(state.request(Cmd::Health).await.map_err(|e| e.to_string())?) diff --git a/app/src-tauri/src/lib.rs b/app/src-tauri/src/lib.rs index 9bf792c..cb7a412 100644 --- a/app/src-tauri/src/lib.rs +++ b/app/src-tauri/src/lib.rs @@ -52,6 +52,7 @@ pub fn run() { bridge::set_zoom, bridge::event_log, bridge::mark_read, + bridge::clear_events, bridge::health, bridge::get_config, bridge::set_config, diff --git a/app/src/App.tsx b/app/src/App.tsx index ab54f28..7f2bf10 100644 --- a/app/src/App.tsx +++ b/app/src/App.tsx @@ -9,7 +9,7 @@ import { Settings } from "./Settings"; import { EventCenter } from "./EventCenter"; import { maybeNotify } from "./notify"; import { COLORS, applyTheme, resolvePalette } from "./theme"; -import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface, getEventLog, markEventsRead, getHealth, closeWorkspaceCmd, getConfig } from "./socketBridge"; +import { getStatusFull, applyPreset, onDaemonEvent, onDaemonRawEvent, setWorkspaceMeta, focusSurface, getEventLog, markEventsRead, clearEvents, getHealth, closeWorkspaceCmd, getConfig } from "./socketBridge"; import type { EventRecord, DaemonHealth, ConfigView } from "./socketBridge"; import { leafIds } from "./layoutTypes"; import type { Group, WorkspaceView, SurfaceState } from "./layoutTypes"; @@ -95,6 +95,8 @@ export function App() { } else if (evt.evt === "events_read") { const ids = new Set(evt.data.ids); setEvents((es) => es.map((e) => (ids.has(e.id) ? { ...e, read: true } : e))); + } else if (evt.evt === "events_cleared") { + setEvents([]); } else if (evt.evt === "state") { setStates((m) => ({ ...m, [evt.data.surface_id]: evt.data.state })); void refresh(); @@ -177,6 +179,7 @@ export function App() { { void markEventsRead({ target: "all" }); }} + onClear={() => { void clearEvents(); }} onSelect={(sid, id) => { void focusSurface(sid); void markEventsRead({ target: "ids", value: [id] }); }} /> )} diff --git a/app/src/EventCenter.tsx b/app/src/EventCenter.tsx index 561d348..242f80f 100644 --- a/app/src/EventCenter.tsx +++ b/app/src/EventCenter.tsx @@ -1,5 +1,5 @@ import { useState } from "react"; -import { Check, Hourglass, X, Power, Send, MessageSquare } from "lucide-react"; +import { Check, Hourglass, X, Power, Send, MessageSquare, Trash2 } from "lucide-react"; import { COLORS, FONT } from "./theme"; import type { EventRecord } from "./socketBridge"; @@ -26,10 +26,11 @@ function rel(ts: number): string { } export function EventCenter({ - events, onMarkAllRead, onSelect, + events, onMarkAllRead, onClear, onSelect, }: { events: EventRecord[]; onMarkAllRead: () => void; + onClear: () => void; onSelect: (surfaceId: string, id: number) => void; }) { const [tab, setTab] = useState("all"); @@ -41,7 +42,14 @@ export function EventCenter({
Event Center - Mark all read + Mark all read + { if (events.length) onClear(); }} + style={{ display: "flex", cursor: events.length ? "pointer" : "default", opacity: events.length ? 1 : 0.4 }} + > + +
diff --git a/app/src/socketBridge.ts b/app/src/socketBridge.ts index 9bae7ff..c774e20 100644 --- a/app/src/socketBridge.ts +++ b/app/src/socketBridge.ts @@ -86,6 +86,10 @@ export async function markEventsRead(target: MarkReadTarget): Promise { await invoke("mark_read", { target }); } +export async function clearEvents(): Promise { + await invoke("clear_events"); +} + export type DaemonEvt = | { evt: "exit"; data: { surface_id: string; code: number } } | { evt: "surface_created"; data: { surface_id: string; workspace_id: string } } @@ -96,7 +100,8 @@ export type DaemonEvt = | { evt: "groups_changed"; data: unknown } | { evt: "config_changed"; data: { config: ConfigView } } | { evt: "event"; data: { record: EventRecord } } - | { evt: "events_read"; data: { ids: number[] } }; + | { evt: "events_read"; data: { ids: number[] } } + | { evt: "events_cleared"; data: unknown }; export function onDaemonEvent(handler: (evt: DaemonEvt) => void): Promise<() => void> { return listen("spacesh:evt", (e) => handler(e.payload)); diff --git a/crates/spacesh-proto/src/message.rs b/crates/spacesh-proto/src/message.rs index 4e23e72..6a8ad93 100644 --- a/crates/spacesh-proto/src/message.rs +++ b/crates/spacesh-proto/src/message.rs @@ -124,6 +124,7 @@ pub enum Cmd { limit: Option, }, MarkRead { target: MarkReadTarget }, + ClearEvents, SetZoom { workspace_id: WorkspaceId, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -163,6 +164,7 @@ pub enum Evt { State { surface_id: SurfaceId, state: SurfaceState }, Event { record: EventRecord }, EventsRead { ids: Vec }, + EventsCleared, ConfigChanged { config: crate::config_view::ConfigView }, } diff --git a/crates/spaceshd/src/event_log.rs b/crates/spaceshd/src/event_log.rs index d60b614..4b71180 100644 --- a/crates/spaceshd/src/event_log.rs +++ b/crates/spaceshd/src/event_log.rs @@ -88,6 +88,11 @@ impl EventLog { changed } + /// Drop all records. `next_id` stays monotonic so ids are never reused. + pub fn clear(&mut self) { + self.records.clear(); + } + pub fn unread_count(&self) -> u32 { self.records.iter().filter(|r| !r.read).count() as u32 } @@ -164,6 +169,18 @@ mod tests { assert!(log.mark_read(&MarkReadTarget::All).is_empty()); } + #[test] + fn clear_drops_records_but_keeps_next_id() { + let mut log = EventLog::new(10); + rec(&mut log, "s_1", EventKind::Done); + rec(&mut log, "s_2", EventKind::Done); + log.clear(); + assert_eq!(log.recent(None).len(), 0); + assert_eq!(log.unread_count(), 0); + // ids continue from where they were — no reuse after a clear. + assert_eq!(rec(&mut log, "s_3", EventKind::Done).id, 3); + } + #[test] fn snapshot_restore_preserves_next_id_and_records() { let mut log = EventLog::new(10); diff --git a/crates/spaceshd/src/server.rs b/crates/spaceshd/src/server.rs index 7cb7319..a1f47cc 100644 --- a/crates/spaceshd/src/server.rs +++ b/crates/spaceshd/src/server.rs @@ -648,6 +648,13 @@ async fn handle_request( let _ = out.send(ok(id, serde_json::Value::Null)).await; } + Cmd::ClearEvents => { + event_log.clear(); + event_persister.mark_dirty(event_log.snapshot()); + broadcast_evt(clients, &Envelope::Evt(Evt::EventsCleared)); + let _ = out.send(ok(id, serde_json::Value::Null)).await; + } + Cmd::Shutdown => { let _ = out.send(ok(id, serde_json::Value::Null)).await; std::process::exit(0);