feat: add todo management feature; implement todo content tracking and display in the UI

This commit is contained in:
asepharyana
2026-07-12 12:43:50 +07:00
parent 80a8223ee3
commit 8ad042139e
6 changed files with 99 additions and 10 deletions
+14
View File
@@ -303,6 +303,20 @@ pub fn apply_action(state: &mut AppStateRest, action: Action) {
state.misc.tick_count = state.misc.tick_count.wrapping_add(1);
let now_ms = chrono::Utc::now().timestamp_millis();
state.misc.drain_expired_toasts(now_ms);
if state.misc.tick_count.is_multiple_of(10) {
let todo_path = state.session_dir.join("todo.md");
if let Ok(content) = std::fs::read_to_string(&todo_path) {
if content != state.misc.todo_content {
state.misc.todo_content = content;
state.dirty = true;
}
} else if !state.misc.todo_content.is_empty() {
state.misc.todo_content.clear();
state.dirty = true;
}
}
// Background API connectivity check — runs on a background thread
// every ~1s while disconnected, every ~30s while connected, so the
// status bar reflects real API availability without user input.
+2
View File
@@ -278,6 +278,7 @@ pub struct MiscState {
pub editor: Option<super::super::mode::editor::EditorState>,
pub api_connected: bool,
pub tick_count: u64,
pub todo_content: String,
}
impl MiscState {
@@ -294,6 +295,7 @@ impl MiscState {
editor: None,
api_connected: false,
tick_count: 0,
todo_content: String::new(),
}
}
+5 -5
View File
@@ -116,7 +116,7 @@ fn run_single_process() -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, crossterm::event::EnableMouseCapture)?;
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
@@ -124,7 +124,7 @@ fn run_single_process() -> Result<()> {
let run_result = run_loop(&mut state, &mut terminal);
let mut restore_stdout = io::stdout();
let _ = execute!(restore_stdout, LeaveAlternateScreen, crossterm::event::DisableMouseCapture);
let _ = execute!(restore_stdout, LeaveAlternateScreen);
let _ = disable_raw_mode();
if let Err(e) = run_result {
@@ -460,7 +460,7 @@ fn run_attach(session_id: &str) -> Result<()> {
enable_raw_mode()?;
let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, crossterm::event::EnableMouseCapture)?;
execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?;
terminal.clear()?;
@@ -544,7 +544,7 @@ fn run_attach(session_id: &str) -> Result<()> {
})?;
}
let _ = execute!(io::stdout(), LeaveAlternateScreen, crossterm::event::DisableMouseCapture);
let _ = execute!(io::stdout(), LeaveAlternateScreen);
let _ = disable_raw_mode();
let _ = client_state.settings.save();
@@ -570,7 +570,7 @@ fn run_loop(
let _ = terminal.clear();
let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen, crossterm::event::DisableMouseCapture);
let _ = execute!(io::stdout(), LeaveAlternateScreen);
}
result
}
+43 -5
View File
@@ -29,6 +29,20 @@ use theme::Theme;
pub fn draw(frame: &mut Frame, state: &crate::app::state::rest::AppStateRest) {
let area = frame.area();
let show_todo = !state.misc.todo_content.is_empty() || state.misc.overlay == crate::app::state::types::Overlay::Todo;
let (main_area, todo_area) = if show_todo && area.width > 60 {
let h_chunks = Layout::default()
.direction(Direction::Horizontal)
.constraints([
Constraint::Min(40),
Constraint::Length(30),
])
.split(area);
(h_chunks[0], Some(h_chunks[1]))
} else {
(area, None)
};
let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
@@ -36,26 +50,50 @@ pub fn draw(frame: &mut Frame, state: &crate::app::state::rest::AppStateRest) {
Constraint::Length(3),
Constraint::Length(1),
])
.split(area);
.split(main_area);
let main_area = chunks[0];
let chat_area = chunks[0];
let input_area = chunks[1];
let status_area = chunks[2];
if state.misc.overlay.is_active() {
if state.misc.overlay.is_active() && state.misc.overlay != crate::app::state::types::Overlay::Todo {
let overlay = state.misc.overlay;
render_overlay(frame, main_area, overlay, state);
render_overlay(frame, chat_area, overlay, state);
} else {
render_main_panel(frame, main_area, state);
render_main_panel(frame, chat_area, state);
}
render_input_bar(frame, input_area, state);
status::draw_status_bar(frame, status_area, state);
if let Some(todo_rect) = todo_area {
render_todo_panel(frame, todo_rect, state);
}
// Toast notifications at top-right (like Hyprland)
render_toasts(frame, state);
}
fn render_todo_panel(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) {
let block = Block::default()
.title(" Todo ")
.borders(Borders::ALL)
.border_style(Style::default().fg(Theme::PRIMARY))
.style(Style::default().bg(Theme::BG));
let content = if state.misc.todo_content.is_empty() {
"No tasks yet."
} else {
&state.misc.todo_content
};
let paragraph = Paragraph::new(content)
.block(block)
.wrap(Wrap { trim: false });
frame.render_widget(paragraph, area);
}
fn render_main_panel(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) {
chat::draw_chat(frame, area, state);
}