diff --git a/apps/infrastructure/src/lib.rs b/apps/infrastructure/src/lib.rs index 186000c..500dc4f 100644 --- a/apps/infrastructure/src/lib.rs +++ b/apps/infrastructure/src/lib.rs @@ -218,6 +218,8 @@ pub enum TurnEvent { agent_name: String, status: crate::AgentStatus, }, + TodoUpdate(String), + PlanUpdate(String), } /// A tool call awaiting execution, along with which execution model diff --git a/apps/infrastructure/src/tools/plan.rs b/apps/infrastructure/src/tools/plan.rs index c1a54a0..df8d3fe 100644 --- a/apps/infrastructure/src/tools/plan.rs +++ b/apps/infrastructure/src/tools/plan.rs @@ -29,8 +29,16 @@ impl Tool for PlanEnter { }) } - fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { + fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let plan_text = crate::tools::arg_str(args, "plan")?; + + let plan_path = ctx.session_dir.join("PLAN.md"); + let _ = std::fs::write(&plan_path, &plan_text); + if let Some(events) = &ctx.turn_events { + let mut q = events.lock().unwrap(); + q.push_back(crate::TurnEvent::PlanUpdate(plan_text.clone())); + } + Ok(format!( "Plan entered (length: {} chars). Waiting for approval...", plan_text.len() @@ -72,6 +80,15 @@ impl Tool for PlanReady { let path = plan_dir.join(&filename); std::fs::write(&path, &plan_content) .map_err(|e| anyhow::anyhow!("failed to save plan: {e}"))?; + + // Also save the latest plan + let plan_path = ctx.session_dir.join("PLAN.md"); + let _ = std::fs::write(&plan_path, &plan_content); + if let Some(events) = &ctx.turn_events { + let mut q = events.lock().unwrap(); + q.push_back(crate::TurnEvent::PlanUpdate(plan_content.clone())); + } + Ok(format!("Plan saved to {filename}. Starting execution.")) } else { Ok("Plan is ready. Starting execution.".to_string()) diff --git a/apps/infrastructure/src/tools/utility/todofinish.rs b/apps/infrastructure/src/tools/utility/todofinish.rs index 93ba41d..358cb23 100644 --- a/apps/infrastructure/src/tools/utility/todofinish.rs +++ b/apps/infrastructure/src/tools/utility/todofinish.rs @@ -28,8 +28,39 @@ impl Tool for Todofinish { }) } - fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { + fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let item = crate::tools::arg_str(args, "item")?; + + let todo_path = ctx.session_dir.join("TODO.md"); + let mut content = std::fs::read_to_string(&todo_path).unwrap_or_default(); + + // Very basic replace to mark as finished + let mut replaced = false; + let lines: Vec<&str> = content.lines().collect(); + let mut new_lines = Vec::new(); + for line in lines { + if line.contains(&item) && line.starts_with("- [") { + let s = line.replacen("- [high]", "- [x]", 1) + .replacen("- [medium]", "- [x]", 1) + .replacen("- [low]", "- [x]", 1); + // if it didn't match those, just replace the first `[` + let s = if s == line { line.replacen("[ ]", "[x]", 1) } else { s }; + new_lines.push(s); + replaced = true; + } else { + new_lines.push(line.to_string()); + } + } + + if replaced { + content = new_lines.join("\n") + "\n"; + let _ = std::fs::write(&todo_path, &content); + if let Some(events) = &ctx.turn_events { + let mut q = events.lock().unwrap(); + q.push_back(crate::TurnEvent::TodoUpdate(content)); + } + } + Ok(format!("TODO completed: {}", item)) } } diff --git a/apps/infrastructure/src/tools/utility/todowrite.rs b/apps/infrastructure/src/tools/utility/todowrite.rs index 6ee2378..b28895c 100644 --- a/apps/infrastructure/src/tools/utility/todowrite.rs +++ b/apps/infrastructure/src/tools/utility/todowrite.rs @@ -33,13 +33,26 @@ impl Tool for Todowrite { }) } - fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result { + fn run(&self, ctx: &ToolCtx, args: &Value) -> Result { let item = crate::tools::arg_str(args, "item")?; let priority = args .get("priority") .and_then(|v| v.as_str()) .unwrap_or("medium"); + let todo_line = format!("- [{}] {}\n", priority, item); + + let todo_path = ctx.session_dir.join("TODO.md"); + let mut content = std::fs::read_to_string(&todo_path).unwrap_or_default(); + content.push_str(&todo_line); + + let _ = std::fs::write(&todo_path, &content); + + if let Some(events) = &ctx.turn_events { + let mut q = events.lock().unwrap(); + q.push_back(crate::TurnEvent::TodoUpdate(content)); + } + Ok(format!("[{}] TODO added: {}", priority, item)) } } diff --git a/apps/interfaces/tui/src/action.rs b/apps/interfaces/tui/src/action.rs index c2752cc..8ad0978 100644 --- a/apps/interfaces/tui/src/action.rs +++ b/apps/interfaces/tui/src/action.rs @@ -163,6 +163,14 @@ pub fn apply_action(state: &mut crate::state::AppStateRest, action: Action) { rt.messages = msgs; } } + zesdex_infrastructure::TurnEvent::TodoUpdate(content) => { + state.misc.todo_content = content; + state.toast_success("TODO list updated."); + } + zesdex_infrastructure::TurnEvent::PlanUpdate(content) => { + state.misc.plan_content = content; + state.toast_success("Project plan updated."); + } zesdex_infrastructure::TurnEvent::Done => { state.turn_in_flight_flag.store(false, std::sync::atomic::Ordering::SeqCst); // Mark dirty so spinner disappears diff --git a/apps/interfaces/tui/src/controller/command.rs b/apps/interfaces/tui/src/controller/command.rs index 37a1d3b..bc6c7b2 100644 --- a/apps/interfaces/tui/src/controller/command.rs +++ b/apps/interfaces/tui/src/controller/command.rs @@ -30,6 +30,8 @@ pub enum Command { Compact, /// `/todo` — open the todo-list overlay. TodoOpen, + /// `/plan` — open the project plan overlay. + PlanOpen, /// `/usage` — open the usage-stats overlay. UsageOpen, /// Catch-all: unrecognised or non-slash input. @@ -82,6 +84,7 @@ pub fn parse_command(text: &str) -> Command { "/model" => Command::ModelList, "/compact" => Command::Compact, "/todo" => Command::TodoOpen, + "/plan" => Command::PlanOpen, "/usage" => Command::UsageOpen, _ => Command::Unknown(cmd.to_string()), }; @@ -129,6 +132,9 @@ pub fn apply_command(cmd: Command) -> Vec { Command::TodoOpen => { vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Todo)] } + Command::PlanOpen => { + vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Plan)] + } Command::UsageOpen => { vec![crate::action::Action::OpenOverlay(crate::state::Overlay::Usage)] } diff --git a/apps/interfaces/tui/src/state.rs b/apps/interfaces/tui/src/state.rs index c718909..6e45593 100644 --- a/apps/interfaces/tui/src/state.rs +++ b/apps/interfaces/tui/src/state.rs @@ -422,6 +422,8 @@ pub enum Overlay { Mcp, /// Task list overlay. Todo, + /// Project plan overlay. + Plan, /// Session rewind / history scrubber. Rewind, /// Learning / lesson management panel. @@ -450,6 +452,7 @@ impl Overlay { Overlay::Effort => "effort", Overlay::Mcp => "mcp", Overlay::Todo => "todo", + Overlay::Plan => "plan", Overlay::Rewind => "rewind", Overlay::Learning => "learning", Overlay::Usage => "usage", @@ -498,6 +501,8 @@ pub struct MiscState { pub tick_count: u64, /// Cached content of the TODO file. pub todo_content: String, + /// Cached content of the PLAN file. + pub plan_content: String, /// Whether a lesson background task is currently running. pub lesson_running: bool, /// Text waiting to be written to the system clipboard. @@ -518,6 +523,7 @@ impl MiscState { api_connected: false, tick_count: 0, todo_content: String::new(), + plan_content: String::new(), lesson_running: false, pending_clipboard_copy: None, } diff --git a/apps/interfaces/tui/src/turn.rs b/apps/interfaces/tui/src/turn.rs index cfc02fa..2384299 100644 --- a/apps/interfaces/tui/src/turn.rs +++ b/apps/interfaces/tui/src/turn.rs @@ -101,24 +101,14 @@ fn run_turn( let tools = all_tools(); let defs = tool_defs(&tools); - // Build tool description list for the system prompt - let tool_desc: Vec = tools.iter().map(|t| { - let params = t.parameters(); - let required = params.get("required").and_then(|r| r.as_array()) - .map(|a| a.iter().filter_map(|v| v.as_str()).collect::>().join(", ")) - .unwrap_or_default(); - format!("- {}: {} (required params: {})", t.name(), t.description(), required) - }).collect(); - let tool_desc_text = tool_desc.join("\n"); - - // Prepend system message with tool descriptions - let sys_msg = ChatMessage::system(format!( - "You are Zesdex, an AI coding agent with access to the following tools:\n\n{}\n\n\ - When using tools, always provide ALL required parameters in your tool call. \ - If a tool returns an error, fix the issue before retrying. \ - Respond conversationally and helpfully.", - tool_desc_text - )); + // Prepend system message + let sys_msg = ChatMessage::system( + "You are Zesdex, an AI coding assistant. You have access to various tools via native function calling to help the user. \ + For any non-trivial tasks, you MUST prioritize creating a structured plan (using `plan_enter`) and a list of TODOs (using `todowrite`) BEFORE executing any other tools or modifying files. \ + Use other tools when necessary. If a tool returns an error, fix the issue before retrying. \ + Respond conversationally, concisely, and helpfully." + .to_string(), + ); messages.insert(0, sys_msg); let tool_ctx = ToolCtx::builder() diff --git a/apps/interfaces/tui/src/view/overlays/mod.rs b/apps/interfaces/tui/src/view/overlays/mod.rs index 74744bf..fba5310 100644 --- a/apps/interfaces/tui/src/view/overlays/mod.rs +++ b/apps/interfaces/tui/src/view/overlays/mod.rs @@ -16,6 +16,7 @@ pub mod quit_confirm; pub mod rewind; pub mod settings; pub mod todo; +pub mod plan; pub mod usage; use crate::state::{AppStateRest, Overlay}; @@ -95,6 +96,9 @@ pub fn render_overlay( Overlay::Todo => { todo::render(frame, overlay_area, block, state); } + Overlay::Plan => { + plan::render(frame, overlay_area, block, state); + } Overlay::Rewind => { rewind::render(frame, overlay_area, block, state); } diff --git a/apps/interfaces/tui/src/view/overlays/plan.rs b/apps/interfaces/tui/src/view/overlays/plan.rs new file mode 100644 index 0000000..68318dc --- /dev/null +++ b/apps/interfaces/tui/src/view/overlays/plan.rs @@ -0,0 +1,32 @@ +//! Overlay: Project Plan view — shows the full project plan content. +use ratatui::style::{Modifier, Style}; +use ratatui::text::Span; +use ratatui::widgets::{Block, Paragraph, Wrap}; +use ratatui::Frame; +use crate::view::theme::Theme; + +/// Render the Project Plan overlay. +pub fn render( + frame: &mut Frame, + area: ratatui::layout::Rect, + block: Block<'static>, + state: &crate::state::AppStateRest, +) { + let block = block + .title(Span::styled( + " Project Plan ", + Style::default() + .fg(Theme::ACCENT_TEAL) + .add_modifier(Modifier::BOLD), + )) + .border_style(Style::default().fg(Theme::ACCENT_TEAL)); + let content = if state.misc.plan_content.is_empty() { + " No active project plan." + } else { + &state.misc.plan_content + }; + let paragraph = Paragraph::new(content) + .block(block) + .wrap(Wrap { trim: false }); + frame.render_widget(paragraph, area); +}