feat(tui): update system message for clarity and conciseness in tool usage instructions

This commit is contained in:
asepharyana
2026-07-20 14:47:55 +07:00
parent fe2e916937
commit 5a373d1031
10 changed files with 130 additions and 21 deletions
+2
View File
@@ -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
+18 -1
View File
@@ -29,8 +29,16 @@ impl Tool for PlanEnter {
})
}
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
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())
@@ -28,8 +28,39 @@ impl Tool for Todofinish {
})
}
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
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))
}
}
@@ -33,13 +33,26 @@ impl Tool for Todowrite {
})
}
fn run(&self, _ctx: &ToolCtx, args: &Value) -> Result<String> {
fn run(&self, ctx: &ToolCtx, args: &Value) -> Result<String> {
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))
}
}
+8
View File
@@ -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
@@ -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<crate::action::Action> {
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)]
}
+6
View File
@@ -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,
}
+8 -18
View File
@@ -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<String> = 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::<Vec<_>>().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()
@@ -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);
}
@@ -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);
}