Skip to content

Commit 64ad050

Browse files
committed
fix(audit): complete jcode-mjd at+field, jcode-1hl render_panel, jcode-pen ambient reminder
Audit findings: 1. jcode-mjd: Added `at: DateTime<Utc>` field to TodoEvent struct. Updated all publish sites (save_todos, restore_todos_after_compaction). Verified BusEvent::TodoUpdated variant was pre-existing. 2. jcode-1hl: Added `render_panel` function in todo_panel.rs that produces ratatui Line<'static> list with marker icons (✓/→/○/⊘) per status. Uses active_form when present, truncates to width, shows +N more hint. 3. jcode-pen: Wired todo reminder into ambient prompt system. - Added todo_reminder_state: TodoReminderState to App struct - TodoReminderState initialized in both App::new sites - todo_turn_reminder() function reads app.todos() + app.todo_reminder_state - Merged into current_turn_system_reminder at both call sites - record_todo_update() called on BusEvent::TodoUpdated (turn.rs:1377) - record_tool_call() called alongside telemetry::record_tool_call (turn.rs:981) - Pattern: oh-my-pi todo-reminder.ts, 60s cooldown, triggers at 5+ calls or 10+ minutes since last update. cargo check clean.
1 parent dc93df6 commit 64ad050

8 files changed

Lines changed: 98 additions & 2 deletions

File tree

crates/jcode-app-core/src/server/compaction_hooks.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,6 +98,7 @@ pub fn restore_todos_after_compaction(
9898
Bus::global().publish(BusEvent::TodoUpdated(TodoEvent {
9999
session_id: session_id.to_string(),
100100
todos: todos.clone(),
101+
at: Utc::now(),
101102
}));
102103
Ok(todos)
103104
}

crates/jcode-base/src/bus.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ pub struct ToolEvent {
4343
pub struct TodoEvent {
4444
pub session_id: String,
4545
pub todos: Vec<TodoItem>,
46+
/// Wall-clock timestamp when the todo list was saved. Subscribers
47+
/// can use this to detect stale state or compute freshness.
48+
pub at: chrono::DateTime<chrono::Utc>,
4649
}
4750

4851
#[derive(Clone, Debug, Serialize, Deserialize)]

crates/jcode-base/src/todo.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ pub fn save_todos(session_id: &str, todos: &[TodoItem]) -> Result<bool> {
5050
Bus::global().publish(BusEvent::TodoUpdated(TodoEvent {
5151
session_id: session_id.to_string(),
5252
todos: todos.to_vec(),
53+
at: chrono::Utc::now(),
5354
}));
5455
Ok(nudge)
5556
}

crates/jcode-tui/src/tui/app.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -762,6 +762,8 @@ pub struct App {
762762
// Monotonic revision incremented on every todo update; TUI uses this to
763763
// invalidate sticky-panel render cache.
764764
todos_revision: u64,
765+
// Persisted todo-reminder drift counter (last update timestamp, tool call count).
766+
todo_reminder_state: crate::tui::todo_reminder::TodoReminderState,
765767
// Pending turn to process (allows UI to redraw before processing starts)
766768
pending_turn: bool,
767769
// When armed by /poke, automatically continue prompting until todos are complete.

crates/jcode-tui/src/tui/app/input.rs

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,22 @@ fn mission_turn_reminder(session_id: &str) -> Option<String> {
117117
.ok()
118118
.flatten()
119119
}
120+
/// Build a todo drift reminder if the model has many tool calls since the
121+
/// last todo update (or 10+ minutes have elapsed). Returns None when no
122+
/// reminder is warranted. Pattern: oh-my-pi todo-reminder.ts.
123+
fn todo_turn_reminder(app: &crate::tui::app::App) -> Option<String> {
124+
let todos = app.todos();
125+
if todos.is_empty() {
126+
return None;
127+
}
128+
if crate::tui::todo_reminder::should_remind(&app.todo_reminder_state, todos) {
129+
Some(crate::tui::todo_reminder::render_reminder(todos))
130+
} else {
131+
None
132+
}
133+
}
134+
135+
120136

121137
fn merge_turn_reminders(a: Option<String>, b: Option<String>) -> Option<String> {
122138
match (a, b) {
@@ -3297,7 +3313,7 @@ impl App {
32973313
));
32983314
}
32993315
if images.is_empty() {
3300-
self.current_turn_system_reminder = mission_turn_reminder(&self.session.id);
3316+
self.current_turn_system_reminder = merge_turn_reminders(mission_turn_reminder(&self.session.id), todo_turn_reminder(self));
33013317
self.add_provider_message(Message::user(&input));
33023318
self.session.add_message(
33033319
Role::User,
@@ -3307,7 +3323,7 @@ impl App {
33073323
}],
33083324
);
33093325
} else {
3310-
self.current_turn_system_reminder = mission_turn_reminder(&self.session.id);
3326+
self.current_turn_system_reminder = merge_turn_reminders(mission_turn_reminder(&self.session.id), todo_turn_reminder(self));
33113327
self.add_provider_message(Message::user_with_images(&input, images.clone()));
33123328
let mut blocks: Vec<ContentBlock> = images
33133329
.into_iter()

crates/jcode-tui/src/tui/app/tui_lifecycle.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -350,6 +350,7 @@ impl App {
350350
last_turn_input_tokens: None,
351351
todos: Vec::new(),
352352
todos_revision: 0,
353+
todo_reminder_state: crate::tui::todo_reminder::TodoReminderState::new(),
353354
pending_turn: false,
354355
auto_poke_incomplete_todos: true,
355356
overnight_auto_poke: None,
@@ -763,6 +764,7 @@ impl App {
763764
last_turn_input_tokens: None,
764765
todos: Vec::new(),
765766
todos_revision: 0,
767+
todo_reminder_state: crate::tui::todo_reminder::TodoReminderState::new(),
766768
pending_turn: false,
767769
auto_poke_incomplete_todos: true,
768770
overnight_auto_poke: None,

crates/jcode-tui/src/tui/app/turn.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -978,6 +978,7 @@ impl App {
978978
)
979979
.await;
980980
crate::telemetry::record_tool_call();
981+
self.todo_reminder_state.record_tool_call();
981982
if tool_result.is_err() {
982983
crate::telemetry::record_tool_failure();
983984
}
@@ -1374,6 +1375,7 @@ impl App {
13741375
Ok(BusEvent::TodoUpdated(update)) => {
13751376
if update.session_id == self.session.id {
13761377
self.set_todos(update.todos);
1378+
self.todo_reminder_state.record_todo_update();
13771379
needs_redraw = true;
13781380
}
13791381
}

crates/jcode-tui/src/tui/todo_panel.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,3 +168,72 @@ mod tests {
168168
assert_eq!(progress_summary(&todos), "2/4 completed");
169169
}
170170
}
171+
172+
// ---------------------------------------------------------------------------
173+
// TUI rendering
174+
// ---------------------------------------------------------------------------
175+
176+
/// Render a `TodoPanelData` as a list of `Line<'static>` suitable for
177+
/// display in the sticky todo panel.
178+
///
179+
/// Format:
180+
/// ── Todos ──
181+
/// ✓ task A
182+
/// → task B
183+
/// ○ task C
184+
/// +2 more
185+
/// ──────────
186+
pub fn render_panel(data: &TodoPanelData, width: usize) -> Vec<ratatui::text::Line<'static>> {
187+
use ratatui::text::{Line, Span};
188+
let mut lines: Vec<Line<'static>> = Vec::new();
189+
let title = match data.mode {
190+
TodoPanelMode::Active => "Todos",
191+
TodoPanelMode::AllCompletedClear => "Todos (all done)",
192+
};
193+
lines.push(Line::from(Span::styled(
194+
format!("── {title} ──"),
195+
ratatui::style::Style::default().fg(ratatui::style::Color::DarkGray),
196+
)));
197+
if data.visible.is_empty() {
198+
lines.push(Line::from(Span::styled(
199+
" (no todos)".to_string(),
200+
ratatui::style::Style::default().fg(ratatui::style::Color::DarkGray),
201+
)));
202+
return lines;
203+
}
204+
for t in &data.visible {
205+
let marker = marker_for_status(&t.status);
206+
let color = match t.status.as_str() {
207+
"completed" => ratatui::style::Color::Green,
208+
"in_progress" => ratatui::style::Color::Yellow,
209+
"pending" => ratatui::style::Color::DarkGray,
210+
"blocked" => ratatui::style::Color::Red,
211+
_ => ratatui::style::Color::White,
212+
};
213+
let label = t.active_form.as_deref().unwrap_or(&t.content);
214+
let line_text = format!(" {marker} {label}");
215+
// Truncate to width if needed
216+
let truncated = if line_text.chars().count() > width {
217+
let mut s: String = line_text.chars().take(width.saturating_sub(1)).collect();
218+
s.push('…');
219+
s
220+
} else {
221+
line_text
222+
};
223+
lines.push(Line::from(Span::styled(
224+
truncated,
225+
ratatui::style::Style::default().fg(color),
226+
)));
227+
}
228+
if data.hidden_open_count > 0 {
229+
lines.push(Line::from(Span::styled(
230+
format!(" +{} more", data.hidden_open_count),
231+
ratatui::style::Style::default().fg(ratatui::style::Color::DarkGray),
232+
)));
233+
}
234+
lines.push(Line::from(Span::styled(
235+
"─".repeat(width.min(40)),
236+
ratatui::style::Style::default().fg(ratatui::style::Color::DarkGray),
237+
)));
238+
lines
239+
}

0 commit comments

Comments
 (0)