@delorenj/claude-notifications 1.2.0 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DO.md +5 -0
- package/FIXES-APPLIED.md +195 -0
- package/INTEGRATION.md +445 -0
- package/LAYOUT-INTEGRATION.md +191 -0
- package/QUICK-REFERENCE.md +195 -0
- package/README.md +145 -14
- package/TASK.md +15 -0
- package/ZELLIJ-NOTIFY.md +523 -0
- package/_bmad-output/implementation-artifacts/spec-install-multi-cli-hooks.md +241 -0
- package/bin/claude-notifications.js +424 -305
- package/bin/claude-notify.js +47 -1
- package/bin/zellij-notify.js +346 -0
- package/bun.lock +35 -0
- package/diagnose-zellij.sh +105 -0
- package/examples/settings-with-zellij.json +18 -0
- package/examples/settings-zellij-only.json +18 -0
- package/examples/zellij-notify-examples.sh +143 -0
- package/lib/adapters/_stub.js +35 -0
- package/lib/adapters/auggie.js +10 -0
- package/lib/adapters/claude-code.js +181 -0
- package/lib/adapters/codex.js +10 -0
- package/lib/adapters/copilot.js +10 -0
- package/lib/adapters/gemini.js +10 -0
- package/lib/adapters/index.js +240 -0
- package/lib/adapters/kimi.js +10 -0
- package/lib/adapters/opencode.js +14 -0
- package/lib/adapters/vibe.js +10 -0
- package/lib/config.js +121 -5
- package/lib/tui.js +115 -0
- package/lib/zellij.js +248 -0
- package/package.json +6 -4
- package/postinstall.js +28 -25
- package/preuninstall.js +18 -9
- package/test/adapters/claude-code.test.js +144 -0
- package/test/adapters/patches.test.js +81 -0
- package/test/adapters/registry.test.js +89 -0
- package/test/adapters/stubs.test.js +46 -0
- package/test/cli-json.test.js +79 -0
- package/test/helpers/fake-fs.js +59 -0
- package/test-integration.sh +113 -0
- package/test-notification-plugin.kdl +34 -0
- package/test-updated-layout.sh +75 -0
- package/test-zellij-cli.sh +72 -0
- package/test.sh +1 -1
- package/zellij-plugin/.cargo/config.toml +5 -0
- package/zellij-plugin/.github/workflows/ci.yml +97 -0
- package/zellij-plugin/Cargo.lock +3558 -0
- package/zellij-plugin/Cargo.toml +40 -0
- package/zellij-plugin/README.md +290 -0
- package/zellij-plugin/build.sh +179 -0
- package/zellij-plugin/configs/examples/accessibility.kdl +31 -0
- package/zellij-plugin/configs/examples/catppuccin.kdl +32 -0
- package/zellij-plugin/configs/examples/default.kdl +34 -0
- package/zellij-plugin/configs/examples/minimal.kdl +22 -0
- package/zellij-plugin/docs/CONFIGURATION.md +191 -0
- package/zellij-plugin/docs/INTEGRATION.md +333 -0
- package/zellij-plugin/src/animation.rs +451 -0
- package/zellij-plugin/src/colors.rs +407 -0
- package/zellij-plugin/src/config.rs +664 -0
- package/zellij-plugin/src/event_bridge.rs +339 -0
- package/zellij-plugin/src/main.rs +420 -0
- package/zellij-plugin/src/notification.rs +466 -0
- package/zellij-plugin/src/queue.rs +399 -0
- package/zellij-plugin/src/renderer.rs +477 -0
- package/zellij-plugin/src/state.rs +338 -0
- package/zellij-plugin/src/tests.rs +413 -0
- package/ruv-swarm-mcp.db +0 -0
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
//! Event Bridge module for Zellij Visual Notifications
|
|
2
|
+
//!
|
|
3
|
+
//! Handles communication with the claude-notifications system via IPC/pipe messages.
|
|
4
|
+
|
|
5
|
+
use serde::{Deserialize, Serialize};
|
|
6
|
+
use crate::notification::{Notification, NotificationBuilder, NotificationType, Priority};
|
|
7
|
+
|
|
8
|
+
/// Event bridge for receiving notifications from claude-notifications
|
|
9
|
+
#[derive(Debug, Default)]
|
|
10
|
+
pub struct EventBridge {
|
|
11
|
+
/// Connection state
|
|
12
|
+
connection_state: ConnectionState,
|
|
13
|
+
/// Protocol version
|
|
14
|
+
protocol_version: String,
|
|
15
|
+
/// Last received message timestamp
|
|
16
|
+
last_message_timestamp: u64,
|
|
17
|
+
/// Error count for retry logic
|
|
18
|
+
error_count: u32,
|
|
19
|
+
/// Maximum errors before fallback
|
|
20
|
+
max_errors: u32,
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/// Connection state for the event bridge
|
|
24
|
+
#[derive(Debug, Clone, PartialEq, Default)]
|
|
25
|
+
pub enum ConnectionState {
|
|
26
|
+
/// Not connected
|
|
27
|
+
#[default]
|
|
28
|
+
Disconnected,
|
|
29
|
+
/// Connecting
|
|
30
|
+
Connecting,
|
|
31
|
+
/// Connected and receiving events
|
|
32
|
+
Connected,
|
|
33
|
+
/// Connection error
|
|
34
|
+
Error(String),
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
impl EventBridge {
|
|
38
|
+
/// Create a new event bridge
|
|
39
|
+
pub fn new() -> Self {
|
|
40
|
+
Self {
|
|
41
|
+
connection_state: ConnectionState::Disconnected,
|
|
42
|
+
protocol_version: "1.0".to_string(),
|
|
43
|
+
last_message_timestamp: 0,
|
|
44
|
+
error_count: 0,
|
|
45
|
+
max_errors: 5,
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/// Get the current connection state
|
|
50
|
+
pub fn connection_state(&self) -> &ConnectionState {
|
|
51
|
+
&self.connection_state
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/// Check if connected
|
|
55
|
+
pub fn is_connected(&self) -> bool {
|
|
56
|
+
matches!(self.connection_state, ConnectionState::Connected)
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/// Parse a notification from a JSON payload
|
|
60
|
+
pub fn parse_notification(&mut self, payload: &str) -> Result<Notification, EventBridgeError> {
|
|
61
|
+
// Try to parse as NotificationMessage first
|
|
62
|
+
match serde_json::from_str::<NotificationMessage>(payload) {
|
|
63
|
+
Ok(msg) => {
|
|
64
|
+
self.connection_state = ConnectionState::Connected;
|
|
65
|
+
self.error_count = 0;
|
|
66
|
+
self.last_message_timestamp = msg.timestamp.unwrap_or(0);
|
|
67
|
+
Ok(self.convert_message_to_notification(msg))
|
|
68
|
+
}
|
|
69
|
+
Err(e) => {
|
|
70
|
+
// Try legacy format
|
|
71
|
+
if let Ok(legacy) = serde_json::from_str::<LegacyNotificationMessage>(payload) {
|
|
72
|
+
self.connection_state = ConnectionState::Connected;
|
|
73
|
+
self.error_count = 0;
|
|
74
|
+
return Ok(self.convert_legacy_to_notification(legacy));
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
self.error_count += 1;
|
|
78
|
+
if self.error_count >= self.max_errors {
|
|
79
|
+
self.connection_state = ConnectionState::Error("Too many parse errors".to_string());
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
Err(EventBridgeError::ParseError(e.to_string()))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/// Convert a NotificationMessage to a Notification
|
|
88
|
+
fn convert_message_to_notification(&self, msg: NotificationMessage) -> Notification {
|
|
89
|
+
let notification_type = msg.notification_type
|
|
90
|
+
.map(|t| NotificationType::from_str(&t))
|
|
91
|
+
.unwrap_or(NotificationType::Attention);
|
|
92
|
+
|
|
93
|
+
let priority = msg.priority
|
|
94
|
+
.map(|p| match p.to_lowercase().as_str() {
|
|
95
|
+
"low" => Priority::Low,
|
|
96
|
+
"normal" => Priority::Normal,
|
|
97
|
+
"high" => Priority::High,
|
|
98
|
+
"critical" => Priority::Critical,
|
|
99
|
+
_ => Priority::from(¬ification_type),
|
|
100
|
+
})
|
|
101
|
+
.unwrap_or_else(|| Priority::from(¬ification_type));
|
|
102
|
+
|
|
103
|
+
let mut builder = NotificationBuilder::new()
|
|
104
|
+
.notification_type(notification_type)
|
|
105
|
+
.message(&msg.message.unwrap_or_else(|| "Claude is waiting...".to_string()))
|
|
106
|
+
.title(&msg.title.unwrap_or_else(|| "Claude Code".to_string()))
|
|
107
|
+
.source(&msg.source.unwrap_or_else(|| "claude-notifications".to_string()))
|
|
108
|
+
.priority(priority)
|
|
109
|
+
.timestamp(msg.timestamp.unwrap_or(0))
|
|
110
|
+
.ttl(msg.ttl_ms.unwrap_or(300_000));
|
|
111
|
+
|
|
112
|
+
// Add pane_id if present
|
|
113
|
+
if let Some(pane_id) = msg.pane_id {
|
|
114
|
+
builder = builder.pane_id(pane_id);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Add tab_index if present
|
|
118
|
+
if let Some(tab_index) = msg.tab_index {
|
|
119
|
+
builder = builder.tab_index(tab_index);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
builder.build()
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// Convert a legacy message format to a Notification
|
|
126
|
+
fn convert_legacy_to_notification(&self, msg: LegacyNotificationMessage) -> Notification {
|
|
127
|
+
Notification::attention(&msg.message)
|
|
128
|
+
.from_source("claude-notifications-legacy")
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/// Handle connection established
|
|
132
|
+
pub fn on_connected(&mut self) {
|
|
133
|
+
self.connection_state = ConnectionState::Connected;
|
|
134
|
+
self.error_count = 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/// Handle connection error
|
|
138
|
+
pub fn on_error(&mut self, error: &str) {
|
|
139
|
+
self.error_count += 1;
|
|
140
|
+
if self.error_count >= self.max_errors {
|
|
141
|
+
self.connection_state = ConnectionState::Error(error.to_string());
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/// Handle connection lost
|
|
146
|
+
pub fn on_disconnected(&mut self) {
|
|
147
|
+
self.connection_state = ConnectionState::Disconnected;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/// Get health status
|
|
151
|
+
pub fn health_status(&self) -> EventBridgeHealth {
|
|
152
|
+
EventBridgeHealth {
|
|
153
|
+
connected: self.is_connected(),
|
|
154
|
+
error_count: self.error_count,
|
|
155
|
+
last_message_timestamp: self.last_message_timestamp,
|
|
156
|
+
protocol_version: self.protocol_version.clone(),
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/// Reset error count (for recovery)
|
|
161
|
+
pub fn reset_errors(&mut self) {
|
|
162
|
+
self.error_count = 0;
|
|
163
|
+
if matches!(self.connection_state, ConnectionState::Error(_)) {
|
|
164
|
+
self.connection_state = ConnectionState::Disconnected;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/// Notification message format from claude-notifications
|
|
170
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
171
|
+
pub struct NotificationMessage {
|
|
172
|
+
/// Protocol version
|
|
173
|
+
#[serde(default)]
|
|
174
|
+
pub version: Option<String>,
|
|
175
|
+
/// Notification type (success, error, warning, info, attention)
|
|
176
|
+
#[serde(rename = "type")]
|
|
177
|
+
pub notification_type: Option<String>,
|
|
178
|
+
/// Message content
|
|
179
|
+
pub message: Option<String>,
|
|
180
|
+
/// Title
|
|
181
|
+
pub title: Option<String>,
|
|
182
|
+
/// Source identifier
|
|
183
|
+
pub source: Option<String>,
|
|
184
|
+
/// Target pane ID
|
|
185
|
+
pub pane_id: Option<u32>,
|
|
186
|
+
/// Target tab index
|
|
187
|
+
pub tab_index: Option<usize>,
|
|
188
|
+
/// Priority (low, normal, high, critical)
|
|
189
|
+
pub priority: Option<String>,
|
|
190
|
+
/// Timestamp (Unix timestamp in milliseconds)
|
|
191
|
+
pub timestamp: Option<u64>,
|
|
192
|
+
/// TTL in milliseconds
|
|
193
|
+
pub ttl_ms: Option<u64>,
|
|
194
|
+
/// Command that triggered the notification
|
|
195
|
+
pub command: Option<String>,
|
|
196
|
+
/// Exit code
|
|
197
|
+
pub exit_code: Option<i32>,
|
|
198
|
+
/// Duration in milliseconds
|
|
199
|
+
pub duration_ms: Option<u64>,
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/// Legacy notification message format (simple JSON)
|
|
203
|
+
#[derive(Debug, Serialize, Deserialize)]
|
|
204
|
+
struct LegacyNotificationMessage {
|
|
205
|
+
/// Message content
|
|
206
|
+
message: String,
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/// Event bridge error types
|
|
210
|
+
#[derive(Debug, Clone)]
|
|
211
|
+
pub enum EventBridgeError {
|
|
212
|
+
/// JSON parse error
|
|
213
|
+
ParseError(String),
|
|
214
|
+
/// Connection error
|
|
215
|
+
ConnectionError(String),
|
|
216
|
+
/// Protocol version mismatch
|
|
217
|
+
VersionMismatch(String),
|
|
218
|
+
/// Invalid message format
|
|
219
|
+
InvalidFormat(String),
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
impl std::fmt::Display for EventBridgeError {
|
|
223
|
+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
224
|
+
match self {
|
|
225
|
+
EventBridgeError::ParseError(e) => write!(f, "Parse error: {}", e),
|
|
226
|
+
EventBridgeError::ConnectionError(e) => write!(f, "Connection error: {}", e),
|
|
227
|
+
EventBridgeError::VersionMismatch(e) => write!(f, "Version mismatch: {}", e),
|
|
228
|
+
EventBridgeError::InvalidFormat(e) => write!(f, "Invalid format: {}", e),
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
/// Event bridge health status
|
|
234
|
+
#[derive(Debug, Clone, Serialize, Deserialize)]
|
|
235
|
+
pub struct EventBridgeHealth {
|
|
236
|
+
/// Whether connected
|
|
237
|
+
pub connected: bool,
|
|
238
|
+
/// Number of errors
|
|
239
|
+
pub error_count: u32,
|
|
240
|
+
/// Last message timestamp
|
|
241
|
+
pub last_message_timestamp: u64,
|
|
242
|
+
/// Protocol version
|
|
243
|
+
pub protocol_version: String,
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/// Create a test notification message (for testing)
|
|
247
|
+
pub fn create_test_message(notification_type: &str, message: &str) -> String {
|
|
248
|
+
let msg = NotificationMessage {
|
|
249
|
+
version: Some("1.0".to_string()),
|
|
250
|
+
notification_type: Some(notification_type.to_string()),
|
|
251
|
+
message: Some(message.to_string()),
|
|
252
|
+
title: Some("Test".to_string()),
|
|
253
|
+
source: Some("test".to_string()),
|
|
254
|
+
pane_id: None,
|
|
255
|
+
tab_index: None,
|
|
256
|
+
priority: None,
|
|
257
|
+
timestamp: Some(0),
|
|
258
|
+
ttl_ms: Some(300_000),
|
|
259
|
+
command: None,
|
|
260
|
+
exit_code: None,
|
|
261
|
+
duration_ms: None,
|
|
262
|
+
};
|
|
263
|
+
serde_json::to_string(&msg).unwrap_or_default()
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
#[cfg(test)]
|
|
267
|
+
mod tests {
|
|
268
|
+
use super::*;
|
|
269
|
+
|
|
270
|
+
#[test]
|
|
271
|
+
fn test_event_bridge_creation() {
|
|
272
|
+
let bridge = EventBridge::new();
|
|
273
|
+
assert!(!bridge.is_connected());
|
|
274
|
+
assert_eq!(bridge.error_count, 0);
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
#[test]
|
|
278
|
+
fn test_parse_notification_message() {
|
|
279
|
+
let mut bridge = EventBridge::new();
|
|
280
|
+
|
|
281
|
+
let json = r#"{
|
|
282
|
+
"version": "1.0",
|
|
283
|
+
"type": "success",
|
|
284
|
+
"message": "Build completed",
|
|
285
|
+
"title": "Claude Code",
|
|
286
|
+
"source": "claude-notifications"
|
|
287
|
+
}"#;
|
|
288
|
+
|
|
289
|
+
let result = bridge.parse_notification(json);
|
|
290
|
+
assert!(result.is_ok());
|
|
291
|
+
|
|
292
|
+
let notif = result.unwrap();
|
|
293
|
+
assert_eq!(notif.notification_type, NotificationType::Success);
|
|
294
|
+
assert_eq!(notif.message, "Build completed");
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#[test]
|
|
298
|
+
fn test_parse_legacy_message() {
|
|
299
|
+
let mut bridge = EventBridge::new();
|
|
300
|
+
|
|
301
|
+
let json = r#"{"message": "Claude is waiting for you..."}"#;
|
|
302
|
+
|
|
303
|
+
let result = bridge.parse_notification(json);
|
|
304
|
+
assert!(result.is_ok());
|
|
305
|
+
|
|
306
|
+
let notif = result.unwrap();
|
|
307
|
+
assert_eq!(notif.notification_type, NotificationType::Attention);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
#[test]
|
|
311
|
+
fn test_parse_error_handling() {
|
|
312
|
+
let mut bridge = EventBridge::new();
|
|
313
|
+
|
|
314
|
+
let invalid_json = "not valid json";
|
|
315
|
+
|
|
316
|
+
for _ in 0..5 {
|
|
317
|
+
let _ = bridge.parse_notification(invalid_json);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
assert!(matches!(bridge.connection_state, ConnectionState::Error(_)));
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
#[test]
|
|
324
|
+
fn test_health_status() {
|
|
325
|
+
let bridge = EventBridge::new();
|
|
326
|
+
let health = bridge.health_status();
|
|
327
|
+
|
|
328
|
+
assert!(!health.connected);
|
|
329
|
+
assert_eq!(health.error_count, 0);
|
|
330
|
+
assert_eq!(health.protocol_version, "1.0");
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
#[test]
|
|
334
|
+
fn test_create_test_message() {
|
|
335
|
+
let msg = create_test_message("success", "Test message");
|
|
336
|
+
assert!(msg.contains("success"));
|
|
337
|
+
assert!(msg.contains("Test message"));
|
|
338
|
+
}
|
|
339
|
+
}
|