- 1
//! Screen 4: Attention Inbox, Memory & Scheduled Automation Tasks. - 2
//! - 3
//! All data is real: - 4
//! - Inbox entries derived from SSE `AgentEvent` (approvals, errors, budget alerts) - 5
//! - Health warnings surfaced as inbox items - 6
//! - Memory store shows real workspace path + health warnings - 7
//! - Skill proposals and scheduled tasks derived from real session/health data - 8
- 9
use ratatui::buffer::Buffer; - 10
use ratatui::layout::{Constraint, Direction, Layout, Rect}; - 11
use ratatui::style::Modifier; - 12
use ratatui::text::{Line, Span}; - 13
use ratatui::widgets::{Block, Borders, Paragraph, Widget}; - 14
- 15
use crate::api::HealthReport; - 16
use crate::app::{DeckFocus, InboxEntry, InboxKind}; - 17
use crate::theme::{Symbols, Theme}; - 18
- 19
pub struct InboxView<'a> { - 20
pub theme: &'a Theme, - 21
pub deck_focus: DeckFocus, - 22
pub alert_acknowledged: bool, - 23
pub skill_promoted: bool, - 24
pub watchdog_restarted: bool, - 25
pub inbox_entries: &'a [InboxEntry], - 26
pub health: &'a HealthReport, - 27
} - 28
- 29
impl<'a> Widget for InboxView<'a> { - 30
fn render(self, area: Rect, buf: &mut Buffer) { - 31
let left_border = if self.deck_focus == DeckFocus::Left { - 32
self.theme.style_border_focus() - 33
} else { - 34
self.theme.style_border() - 35
}; - 36
let right_border = if self.deck_focus == DeckFocus::Right { - 37
self.theme.style_border_focus() - 38
} else { - 39
self.theme.style_border() - 40
}; - 41
- 42
let deck_chunks = Layout::default() - 43
.direction(Direction::Horizontal) - 44
.constraints([Constraint::Percentage(60), Constraint::Percentage(40)]) - 45
.split(area); - 46
- 47
// ------------------------------------------------------------- - 48
// LEFT DECK: Prioritized Attention Inbox - 49
// ------------------------------------------------------------- - 50
let inbox_lines: Vec<Line> = if self.inbox_entries.is_empty() { - 51
vec![Line::from(vec![Span::styled( - 52
"Inbox is clear — awaiting real events…", - 53
self.theme.style_card().add_modifier(Modifier::DIM), - 54
)])] - 55
} else { - 56
self.inbox_entries - 57
.iter() - 58
.rev() - 59
.take(5) - 60
.map(|entry| { - 61
let (icon, style) = self.icon_for_kind(&entry.kind); - 62
let action_hint = match &entry.actionable { - 63
Some(_) => " [ [a] Ack ] [ [p] Promote ] [ [r] Restart ]".to_string(), - 64
None => String::new(), - 65
}; - 66
Line::from(vec![ - 67
Span::styled( - 68
format!("[{}] ", entry.timestamp), - 69
self.theme.style_card().add_modifier(Modifier::DIM), - 70
), - 71
Span::styled(format!("{icon} [{:?}] ", entry.kind), style), - 72
Span::styled( - 73
&entry.title, - 74
self.theme.style_card().add_modifier(Modifier::BOLD), - 75
), - 76
Span::styled(&entry.body, self.theme.style_card()), - 77
Span::styled( - 78
action_hint, - 79
self.theme.style_card().add_modifier(Modifier::DIM), - 80
), - 81
]) - 82
}) - 83
.collect() - 84
}; - 85
- 86
let unread_count = self - 87
.inbox_entries - 88
.iter() - 89
.filter(|e| !matches!(e.kind, InboxKind::Heartbeat)) - 90
.count(); - 91
- 92
let inbox_block = Block::default() - 93
.borders(Borders::ALL) - 94
.border_style(left_border) - 95
.title(format!( - 96
" {} Attention Inbox ({} Unread) [Tab] ", - 97
Symbols::STATUS_ACTIVE, - 98
unread_count - 99
)) - 100
.title_style(self.theme.style_accent().add_modifier(Modifier::BOLD)); - 101
- 102
Paragraph::new(inbox_lines) - 103
.block(inbox_block) - 104
.render(deck_chunks[0], buf); - 105
- 106
// ------------------------------------------------------------- - 107
// RIGHT DECK: Durable Memory, Skill Proposals, Scheduled Tasks - 108
// ------------------------------------------------------------- - 109
let right_chunks = Layout::default() - 110
.direction(Direction::Vertical) - 111
.constraints([ - 112
Constraint::Percentage(40), - 113
Constraint::Percentage(30), - 114
Constraint::Percentage(30), - 115
]) - 116
.split(deck_chunks[1]); - 117
- 118
// Memory Store — real from health cwd + warnings - 119
let mem_lines: Vec<Line> = { - 120
let mut lines = vec![Line::from(vec![Span::styled( - 121
"Workspace Memory:", - 122
self.theme.style_card().add_modifier(Modifier::BOLD), - 123
)])]; - 124
lines.push(Line::from(vec![ - 125
Span::styled( - 126
format!(" {} Path: ", Symbols::BULLET), - 127
self.theme.style_accent(), - 128
), - 129
Span::styled(&self.health.cwd, self.theme.style_card()), - 130
])); - 131
if !self.health.warnings.is_empty() { - 132
for w in &self.health.warnings { - 133
lines.push(Line::from(vec![ - 134
Span::styled( - 135
format!(" {} Warning: ", Symbols::STATUS_ACTIVE), - 136
self.theme.style_warn(), - 137
), - 138
Span::styled(w, self.theme.style_card()), - 139
])); - 140
} - 141
} else { - 142
lines.push(Line::from(vec![Span::styled( - 143
" (no warnings)", - 144
self.theme.style_card().add_modifier(Modifier::DIM), - 145
)])); - 146
} - 147
lines - 148
}; - 149
- 150
let mem_block = Block::default() - 151
.borders(Borders::ALL) - 152
.border_style(right_border) - 153
.title(format!(" {} Durable Memory Store ", Symbols::BULLET)) - 154
.title_style(self.theme.style_card()); - 155
- 156
Paragraph::new(mem_lines) - 157
.block(mem_block) - 158
.render(right_chunks[0], buf); - 159
- 160
// Skill Proposals Queue — real from inbox entries - 161
let prop_entries: Vec<&InboxEntry> = self - 162
.inbox_entries - 163
.iter() - 164
.filter(|e| e.kind == InboxKind::SkillProposal) - 165
.collect(); - 166
- 167
let prop_lines: Vec<Line> = if prop_entries.is_empty() { - 168
vec![Line::from(vec![Span::styled( - 169
"(no skill proposals pending)", - 170
self.theme.style_card().add_modifier(Modifier::DIM), - 171
)])] - 172
} else { - 173
let mut lines = Vec::new(); - 174
for entry in prop_entries.iter().take(3) { - 175
lines.push(Line::from(vec![Span::styled( - 176
entry.title.clone(), - 177
self.theme.style_card().add_modifier(Modifier::BOLD), - 178
)])); - 179
lines.push(Line::from(vec![Span::styled( - 180
if self.skill_promoted { - 181
" [ ✓ PROMOTED TO PIPELINE ]" - 182
} else { - 183
" [ [p] Promote ] [ [r] Reject ]" - 184
}, - 185
if self.skill_promoted { - 186
self.theme.style_ok().add_modifier(Modifier::BOLD) - 187
} else { - 188
self.theme.style_card().add_modifier(Modifier::DIM) - 189
}, - 190
)])); - 191
} - 192
lines - 193
}; - 194
- 195
let prop_block = Block::default() - 196
.borders(Borders::ALL) - 197
.border_style(right_border) - 198
.title(" Skill Proposals Queue ") - 199
.title_style(self.theme.style_card()); - 200
- 201
Paragraph::new(prop_lines) - 202
.block(prop_block) - 203
.render(right_chunks[1], buf); - 204
- 205
// Scheduled Tasks & Automation — real from inbox entries - 206
let task_entries: Vec<&InboxEntry> = self - 207
.inbox_entries - 208
.iter() - 209
.filter(|e| e.kind == InboxKind::ScheduledTask) - 210
.collect(); - 211
- 212
let task_lines: Vec<Line> = if task_entries.is_empty() { - 213
vec![Line::from(vec![Span::styled( - 214
"(no scheduled tasks reported)", - 215
self.theme.style_card().add_modifier(Modifier::DIM), - 216
)])] - 217
} else { - 218
task_entries - 219
.iter() - 220
.take(4) - 221
.map(|entry| { - 222
let status_icon = if entry.body.contains("Next:") { - 223
Symbols::STATUS_ACTIVE - 224
} else { - 225
Symbols::CHECK - 226
}; - 227
Line::from(vec![ - 228
Span::styled( - 229
format!("{} {} ", status_icon, entry.title), - 230
self.theme.style_ok(), - 231
), - 232
Span::styled( - 233
&entry.body, - 234
self.theme.style_card().add_modifier(Modifier::DIM), - 235
), - 236
]) - 237
}) - 238
.collect() - 239
}; - 240
- 241
let task_block = Block::default() - 242
.borders(Borders::ALL) - 243
.border_style(right_border) - 244
.title(" Scheduled Tasks & Automation ") - 245
.title_style(self.theme.style_card()); - 246
- 247
Paragraph::new(task_lines) - 248
.block(task_block) - 249
.render(right_chunks[2], buf); - 250
} - 251
} - 252
- 253
impl<'a> InboxView<'a> { - 254
fn icon_for_kind(&self, kind: &InboxKind) -> (&'static str, ratatui::style::Style) { - 255
match kind { - 256
InboxKind::BudgetAlert => ( - 257
Symbols::CROSS, - 258
self.theme.style_danger().add_modifier(Modifier::BOLD), - 259
), - 260
InboxKind::ApprovalPending => ( - 261
Symbols::STATUS_ACTIVE, - 262
self.theme.style_warn().add_modifier(Modifier::BOLD), - 263
), - 264
InboxKind::ApprovalDenied => ( - 265
Symbols::CROSS, - 266
self.theme.style_danger().add_modifier(Modifier::BOLD), - 267
), - 268
InboxKind::WatchdogFailure => ( - 269
Symbols::CROSS, - 270
self.theme.style_danger().add_modifier(Modifier::BOLD), - 271
), - 272
InboxKind::SkillProposal => ( - 273
Symbols::STATUS_ACTIVE, - 274
self.theme.style_info().add_modifier(Modifier::BOLD), - 275
), - 276
InboxKind::ScheduledTask => ( - 277
Symbols::STATUS_ACTIVE, - 278
self.theme.style_ok().add_modifier(Modifier::BOLD), - 279
), - 280
InboxKind::Heartbeat => ( - 281
Symbols::STATUS_IDLE, - 282
self.theme.style_card().add_modifier(Modifier::DIM), - 283
), - 284
} - 285
} - 286
} - 287
Indexing the workspace…
Vakyartha documentation is discovering safe artifacts, anchors, and source references.