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
Generated
+34
View File
@@ -812,6 +812,15 @@ dependencies = [
"miniz_oxide", "miniz_oxide",
] ]
[[package]]
name = "fluent-uri"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17c704e9dbe1ddd863da1e6ff3567795087b1eb201ce80d8fa81162e1516500d"
dependencies = [
"bitflags 1.3.2",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@@ -1638,6 +1647,19 @@ dependencies = [
"hashbrown 0.17.1", "hashbrown 0.17.1",
] ]
[[package]]
name = "lsp-types"
version = "0.97.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53353550a17c04ac46c585feb189c2db82154fc84b79c7a66c96c2c644f66071"
dependencies = [
"bitflags 1.3.2",
"fluent-uri",
"serde",
"serde_json",
"serde_repr",
]
[[package]] [[package]]
name = "mac_address" name = "mac_address"
version = "1.1.8" version = "1.1.8"
@@ -2805,6 +2827,17 @@ dependencies = [
"zmij", "zmij",
] ]
[[package]]
name = "serde_repr"
version = "0.1.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.118",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@@ -4288,6 +4321,7 @@ dependencies = [
"include_dir", "include_dir",
"infer", "infer",
"libc", "libc",
"lsp-types",
"percent-encoding", "percent-encoding",
"pulldown-cmark", "pulldown-cmark",
"ratatui", "ratatui",
+1
View File
@@ -37,6 +37,7 @@ tracing = "0.1"
chrono = { version = "0.4", features = ["serde"] } chrono = { version = "0.4", features = ["serde"] }
tracing-subscriber = { version = "0.3", features = ["env-filter"] } tracing-subscriber = { version = "0.3", features = ["env-filter"] }
webbrowser = "1" webbrowser = "1"
lsp-types = "0.97"
[[bin]] [[bin]]
name = "zesdex" name = "zesdex"
+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); state.misc.tick_count = state.misc.tick_count.wrapping_add(1);
let now_ms = chrono::Utc::now().timestamp_millis(); let now_ms = chrono::Utc::now().timestamp_millis();
state.misc.drain_expired_toasts(now_ms); 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 // Background API connectivity check — runs on a background thread
// every ~1s while disconnected, every ~30s while connected, so the // every ~1s while disconnected, every ~30s while connected, so the
// status bar reflects real API availability without user input. // 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 editor: Option<super::super::mode::editor::EditorState>,
pub api_connected: bool, pub api_connected: bool,
pub tick_count: u64, pub tick_count: u64,
pub todo_content: String,
} }
impl MiscState { impl MiscState {
@@ -294,6 +295,7 @@ impl MiscState {
editor: None, editor: None,
api_connected: false, api_connected: false,
tick_count: 0, tick_count: 0,
todo_content: String::new(),
} }
} }
+5 -5
View File
@@ -116,7 +116,7 @@ fn run_single_process() -> Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, crossterm::event::EnableMouseCapture)?; execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
terminal.clear()?; terminal.clear()?;
@@ -124,7 +124,7 @@ fn run_single_process() -> Result<()> {
let run_result = run_loop(&mut state, &mut terminal); let run_result = run_loop(&mut state, &mut terminal);
let mut restore_stdout = io::stdout(); let mut restore_stdout = io::stdout();
let _ = execute!(restore_stdout, LeaveAlternateScreen, crossterm::event::DisableMouseCapture); let _ = execute!(restore_stdout, LeaveAlternateScreen);
let _ = disable_raw_mode(); let _ = disable_raw_mode();
if let Err(e) = run_result { if let Err(e) = run_result {
@@ -460,7 +460,7 @@ fn run_attach(session_id: &str) -> Result<()> {
enable_raw_mode()?; enable_raw_mode()?;
let mut stdout = io::stdout(); let mut stdout = io::stdout();
execute!(stdout, EnterAlternateScreen, crossterm::event::EnableMouseCapture)?; execute!(stdout, EnterAlternateScreen)?;
let backend = CrosstermBackend::new(stdout); let backend = CrosstermBackend::new(stdout);
let mut terminal = Terminal::new(backend)?; let mut terminal = Terminal::new(backend)?;
terminal.clear()?; 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 _ = disable_raw_mode();
let _ = client_state.settings.save(); let _ = client_state.settings.save();
@@ -570,7 +570,7 @@ fn run_loop(
let _ = terminal.clear(); let _ = terminal.clear();
let _ = disable_raw_mode(); let _ = disable_raw_mode();
let _ = execute!(io::stdout(), LeaveAlternateScreen, crossterm::event::DisableMouseCapture); let _ = execute!(io::stdout(), LeaveAlternateScreen);
} }
result result
} }
+43 -5
View File
@@ -29,6 +29,20 @@ use theme::Theme;
pub fn draw(frame: &mut Frame, state: &crate::app::state::rest::AppStateRest) { pub fn draw(frame: &mut Frame, state: &crate::app::state::rest::AppStateRest) {
let area = frame.area(); 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() let chunks = Layout::default()
.direction(Direction::Vertical) .direction(Direction::Vertical)
.constraints([ .constraints([
@@ -36,26 +50,50 @@ pub fn draw(frame: &mut Frame, state: &crate::app::state::rest::AppStateRest) {
Constraint::Length(3), Constraint::Length(3),
Constraint::Length(1), Constraint::Length(1),
]) ])
.split(area); .split(main_area);
let main_area = chunks[0]; let chat_area = chunks[0];
let input_area = chunks[1]; let input_area = chunks[1];
let status_area = chunks[2]; 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; let overlay = state.misc.overlay;
render_overlay(frame, main_area, overlay, state); render_overlay(frame, chat_area, overlay, state);
} else { } else {
render_main_panel(frame, main_area, state); render_main_panel(frame, chat_area, state);
} }
render_input_bar(frame, input_area, state); render_input_bar(frame, input_area, state);
status::draw_status_bar(frame, status_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) // Toast notifications at top-right (like Hyprland)
render_toasts(frame, state); 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) { fn render_main_panel(frame: &mut Frame, area: Rect, state: &crate::app::state::rest::AppStateRest) {
chat::draw_chat(frame, area, state); chat::draw_chat(frame, area, state);
} }