Merge events-clear-and-settings-x: clear events + settings close button

This commit is contained in:
2026-06-15 13:38:35 +07:00
9 changed files with 61 additions and 6 deletions
+5
View File
@@ -430,6 +430,11 @@ pub async fn mark_read(state: BridgeState<'_>, target: Value) -> Result<Value, S
data_of(state.request(Cmd::MarkRead { target }).await.map_err(|e| e.to_string())?)
}
#[tauri::command]
pub async fn clear_events(state: BridgeState<'_>) -> Result<Value, String> {
data_of(state.request(Cmd::ClearEvents).await.map_err(|e| e.to_string())?)
}
#[tauri::command]
pub async fn health(state: BridgeState<'_>) -> Result<Value, String> {
data_of(state.request(Cmd::Health).await.map_err(|e| e.to_string())?)
+1
View File
@@ -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,
+4 -1
View File
@@ -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() {
<EventCenter
events={events}
onMarkAllRead={() => { void markEventsRead({ target: "all" }); }}
onClear={() => { void clearEvents(); }}
onSelect={(sid, id) => { void focusSurface(sid); void markEventsRead({ target: "ids", value: [id] }); }}
/>
)}
+11 -3
View File
@@ -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<Tab>("all");
@@ -41,7 +42,14 @@ export function EventCenter({
<div style={{ display: "flex", flexDirection: "column", width: 300, flex: "0 0 300px", background: COLORS.bgSidebar, height: "100%", padding: 14, boxSizing: "border-box", borderLeft: `1px solid ${COLORS.borderSubtle}` }}>
<div style={{ display: "flex", alignItems: "center", marginBottom: 12 }}>
<span style={{ fontFamily: FONT.ui, fontSize: 13, fontWeight: 700, color: COLORS.textPrimary, flex: 1 }}>Event Center</span>
<span onClick={onMarkAllRead} style={{ fontFamily: FONT.ui, fontSize: 11, color: COLORS.accent, cursor: "pointer" }}>Mark all read</span>
<span onClick={onMarkAllRead} style={{ fontFamily: FONT.ui, fontSize: 11, color: COLORS.accent, cursor: "pointer", marginRight: 10 }}>Mark all read</span>
<span
title="Clear all events"
onClick={() => { if (events.length) onClear(); }}
style={{ display: "flex", cursor: events.length ? "pointer" : "default", opacity: events.length ? 1 : 0.4 }}
>
<Trash2 size={14} color={COLORS.stError} aria-label="Clear all" />
</span>
</div>
<div style={{ display: "flex", gap: 6, marginBottom: 12 }}>
+8 -1
View File
@@ -1,4 +1,5 @@
import { useEffect, useRef, useState } from "react";
import { X } from "lucide-react";
import { COLORS, FONT, ACCENTS } from "./theme";
import { setConfig, restartDaemon } from "./socketBridge";
import type { ConfigView, DaemonHealth } from "./socketBridge";
@@ -20,7 +21,13 @@ export function Settings({ config, health, onClose, onReload }: { config: Config
<div onMouseDown={onClose} style={{ position: "fixed", inset: 0, zIndex: 2000, background: "#000A", display: "flex", alignItems: "center", justifyContent: "center" }}>
<div ref={ref} tabIndex={-1} onMouseDown={(e) => e.stopPropagation()} onKeyDown={(e) => { e.stopPropagation(); if (e.key === "Escape") onClose(); }}
style={{ width: 520, maxHeight: "80vh", overflowY: "auto", background: COLORS.bgApp, border: `1px solid ${COLORS.borderStrong}`, borderRadius: 14, padding: 24, color: COLORS.textPrimary, fontFamily: FONT.ui }}>
<div style={{ fontWeight: 700, fontSize: 16, marginBottom: 16 }}>Settings</div>
<div style={{ display: "flex", alignItems: "center", marginBottom: 16 }}>
<span style={{ fontWeight: 700, fontSize: 16, flex: 1 }}>Settings</span>
<button onClick={onClose} aria-label="Close" title="Close (Esc)"
style={{ display: "flex", alignItems: "center", justifyContent: "center", width: 26, height: 26, borderRadius: 6, background: "transparent", border: "none", color: COLORS.textMuted, cursor: "pointer" }}>
<X size={16} />
</button>
</div>
<div style={{ fontSize: 12, color: COLORS.textSecondary, marginBottom: 6 }}>Terminal font</div>
<select value={config.font_family} onChange={(e) => void setConfig({ font_family: e.target.value })}
+6 -1
View File
@@ -86,6 +86,10 @@ export async function markEventsRead(target: MarkReadTarget): Promise<void> {
await invoke("mark_read", { target });
}
export async function clearEvents(): Promise<void> {
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<DaemonEvt>("spacesh:evt", (e) => handler(e.payload));
+2
View File
@@ -124,6 +124,7 @@ pub enum Cmd {
limit: Option<u32>,
},
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<u64> },
EventsCleared,
ConfigChanged { config: crate::config_view::ConfigView },
}
+17
View File
@@ -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);
+7
View File
@@ -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);