feat(tui): Sprint 5 — chat modes, /diff, /undo, /ls commands
- Create src/chat_mode.rs: ChatMode enum (Code/Ask/Architect) with system_prompt_suffix() for model behavior control - /code, /ask, /architect slash commands to switch modes - /diff — shows uncommitted git changes in conversation (no --color=always) - /undo — shows revertable changes, /undo --confirm to actually revert - /ls <path> — reads file contents into conversation - /theme <name> — switches between 11 built-in themes at runtime - /keys <preset> — switches Emacs/Vim/Windows keybindings - ChatMode field in TuiApp, integrated with dashboard display - 267 tests pass Sprint 5 of 7. Authored by TheArchitectit
This commit is contained in:
parent
faf2ab831b
commit
81beafa2a1
|
|
@ -0,0 +1,83 @@
|
||||||
|
//! Chat mode — controls AI behavior (code/ask/architect).
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub enum ChatMode {
|
||||||
|
/// Default — full access, edits files and runs commands.
|
||||||
|
Code,
|
||||||
|
/// Discussion only — no file changes.
|
||||||
|
Ask,
|
||||||
|
/// Plan first, then implement after approval.
|
||||||
|
Architect,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ChatMode {
|
||||||
|
pub fn label(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ChatMode::Code => "code",
|
||||||
|
ChatMode::Ask => "ask",
|
||||||
|
ChatMode::Architect => "arch",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn description(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ChatMode::Code => "Full access — edits and runs code",
|
||||||
|
ChatMode::Ask => "Discussion only — no changes made",
|
||||||
|
ChatMode::Architect => "Plan first, then implement",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Appended to system prompt to instruct the model about mode constraints.
|
||||||
|
pub fn system_prompt_suffix(&self) -> &'static str {
|
||||||
|
match self {
|
||||||
|
ChatMode::Code => "",
|
||||||
|
ChatMode::Ask => "\n\nIMPORTANT: Do NOT modify any files. Only discuss and explain.",
|
||||||
|
ChatMode::Architect => {
|
||||||
|
"\n\nIMPORTANT: First create a plan. After the user approves, implement it."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cycle to the next mode.
|
||||||
|
pub fn next(self) -> Self {
|
||||||
|
match self {
|
||||||
|
ChatMode::Code => ChatMode::Ask,
|
||||||
|
ChatMode::Ask => ChatMode::Architect,
|
||||||
|
ChatMode::Architect => ChatMode::Code,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_cycle() {
|
||||||
|
assert_eq!(ChatMode::Code.next(), ChatMode::Ask);
|
||||||
|
assert_eq!(ChatMode::Ask.next(), ChatMode::Architect);
|
||||||
|
assert_eq!(ChatMode::Architect.next(), ChatMode::Code);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_labels() {
|
||||||
|
assert_eq!(ChatMode::Code.label(), "code");
|
||||||
|
assert_eq!(ChatMode::Ask.label(), "ask");
|
||||||
|
assert_eq!(ChatMode::Architect.label(), "arch");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_code_mode_has_no_suffix() {
|
||||||
|
assert_eq!(ChatMode::Code.system_prompt_suffix(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_ask_mode_restrains() {
|
||||||
|
assert!(ChatMode::Ask.system_prompt_suffix().contains("Do NOT modify"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_architect_mode_plans_first() {
|
||||||
|
assert!(ChatMode::Architect.system_prompt_suffix().contains("plan"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -18,6 +18,7 @@ mod init;
|
||||||
mod input;
|
mod input;
|
||||||
mod render;
|
mod render;
|
||||||
mod setup_wizard;
|
mod setup_wizard;
|
||||||
|
mod chat_mode;
|
||||||
mod command_palette;
|
mod command_palette;
|
||||||
mod keybindings;
|
mod keybindings;
|
||||||
mod markdown;
|
mod markdown;
|
||||||
|
|
@ -7181,6 +7182,112 @@ fn run_tui_repl(mut cli: LiveCli) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Slash commands handled locally in TUI (not sent to model)
|
||||||
|
if trimmed.starts_with("/theme") {
|
||||||
|
let args = trimmed.strip_prefix("/theme").unwrap_or("").trim();
|
||||||
|
if args.is_empty() {
|
||||||
|
let names = crate::theme::TuiTheme::all_builtin_names();
|
||||||
|
app.push_system_message(&format!(
|
||||||
|
"Available themes: {}\nUsage: /theme <name>",
|
||||||
|
names.join(", ")
|
||||||
|
));
|
||||||
|
} else if let Some(theme) = crate::theme::TuiTheme::builtin(args) {
|
||||||
|
app.set_theme(theme);
|
||||||
|
app.push_system_message(&format!("Theme: {args}"));
|
||||||
|
} else {
|
||||||
|
app.push_system_message(&format!("Unknown theme: {args}"));
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if trimmed.starts_with("/keys") {
|
||||||
|
let args = trimmed.strip_prefix("/keys").unwrap_or("").trim();
|
||||||
|
match args {
|
||||||
|
"emacs" => { app.keymap.set_preset(crate::keybindings::KeyPreset::Emacs); app.push_system_message("Keys: Emacs"); }
|
||||||
|
"vim" => { app.keymap.set_preset(crate::keybindings::KeyPreset::Vim); app.push_system_message("Keys: Vim — i for insert, Esc for normal"); }
|
||||||
|
"windows" => { app.keymap.set_preset(crate::keybindings::KeyPreset::Windows); app.push_system_message("Keys: Windows"); }
|
||||||
|
"" => {
|
||||||
|
app.push_system_message(&format!("Current: {:?}\nAvailable: emacs, vim, windows\nUsage: /keys <preset>", app.keymap.preset()));
|
||||||
|
}
|
||||||
|
_ => { app.push_system_message(&format!("Unknown: {args}. Available: emacs, vim, windows")); }
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if matches!(trimmed.as_str(), "/code" | "/ask" | "/architect" | "/arch") {
|
||||||
|
let mode = match trimmed.as_str() {
|
||||||
|
"/code" => crate::chat_mode::ChatMode::Code,
|
||||||
|
"/ask" => crate::chat_mode::ChatMode::Ask,
|
||||||
|
_ => crate::chat_mode::ChatMode::Architect,
|
||||||
|
};
|
||||||
|
app.chat_mode = mode;
|
||||||
|
app.push_system_message(&format!("Mode: {} — {}", mode.label(), mode.description()));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if trimmed == "/diff" {
|
||||||
|
let output = std::process::Command::new("git")
|
||||||
|
.args(["diff"])
|
||||||
|
.current_dir(std::env::current_dir().unwrap_or_default())
|
||||||
|
.output();
|
||||||
|
match output {
|
||||||
|
Ok(out) if out.status.success() => {
|
||||||
|
let diff = String::from_utf8_lossy(&out.stdout);
|
||||||
|
if diff.is_empty() {
|
||||||
|
app.push_system_message("No uncommitted changes.");
|
||||||
|
} else {
|
||||||
|
app.push_diff(&diff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(out) => {
|
||||||
|
let err = String::from_utf8_lossy(&out.stderr);
|
||||||
|
app.push_system_message(&format!("git diff failed: {err}"));
|
||||||
|
}
|
||||||
|
Err(e) => { app.push_system_message(&format!("Failed to run git: {e}")); }
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if trimmed.starts_with("/undo") {
|
||||||
|
let args = trimmed.strip_prefix("/undo").unwrap_or("").trim();
|
||||||
|
if args == "--confirm" || args == "-y" {
|
||||||
|
let output = std::process::Command::new("git")
|
||||||
|
.args(["checkout", "--", "."])
|
||||||
|
.current_dir(std::env::current_dir().unwrap_or_default())
|
||||||
|
.output();
|
||||||
|
match output {
|
||||||
|
Ok(out) if out.status.success() => app.push_system_message("✓ Reverted all uncommitted changes."),
|
||||||
|
Ok(out) => { let err = String::from_utf8_lossy(&out.stderr); app.push_system_message(&format!("Undo failed: {err}")); }
|
||||||
|
Err(e) => { app.push_system_message(&format!("Failed to run git: {e}")); }
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let output = std::process::Command::new("git")
|
||||||
|
.args(["diff", "--stat"])
|
||||||
|
.current_dir(std::env::current_dir().unwrap_or_default())
|
||||||
|
.output();
|
||||||
|
let stat = output.ok().filter(|o| o.status.success())
|
||||||
|
.map(|o| String::from_utf8_lossy(&o.stdout).to_string())
|
||||||
|
.unwrap_or_default();
|
||||||
|
if stat.is_empty() {
|
||||||
|
app.push_system_message("Nothing to undo — no uncommitted changes.");
|
||||||
|
} else {
|
||||||
|
app.push_system_message(&format!("This will revert:\n{stat}\nType /undo --confirm to proceed."));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if trimmed.starts_with("/ls") {
|
||||||
|
let path = trimmed.strip_prefix("/ls").unwrap_or("").trim();
|
||||||
|
if path.is_empty() {
|
||||||
|
app.push_system_message("Usage: /ls <file-path>");
|
||||||
|
} else {
|
||||||
|
match std::fs::read_to_string(path) {
|
||||||
|
Ok(content) => {
|
||||||
|
app.push_system_message(&format!("── {path} ──"));
|
||||||
|
app.push_output(&content, false);
|
||||||
|
}
|
||||||
|
Err(e) => app.push_system_message(&format!("Cannot read {path}: {e}")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
app.push_user_input(&input);
|
app.push_user_input(&input);
|
||||||
cli.record_prompt_history(&trimmed);
|
cli.record_prompt_history(&trimmed);
|
||||||
update_dashboard(&dashboard_state, &cli);
|
update_dashboard(&dashboard_state, &cli);
|
||||||
|
|
|
||||||
|
|
@ -182,6 +182,7 @@ pub struct TuiApp {
|
||||||
pub theme: crate::theme::TuiTheme,
|
pub theme: crate::theme::TuiTheme,
|
||||||
pub keymap: crate::keybindings::KeyMap,
|
pub keymap: crate::keybindings::KeyMap,
|
||||||
pub command_palette: crate::command_palette::CommandPalette,
|
pub command_palette: crate::command_palette::CommandPalette,
|
||||||
|
pub chat_mode: crate::chat_mode::ChatMode,
|
||||||
}
|
}
|
||||||
|
|
||||||
const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
const SPINNER_FRAMES: &[&str] = &["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
||||||
|
|
@ -238,6 +239,7 @@ impl TuiApp {
|
||||||
theme: crate::theme::TuiTheme::builtin("default").unwrap(),
|
theme: crate::theme::TuiTheme::builtin("default").unwrap(),
|
||||||
keymap: crate::keybindings::KeyMap::new(crate::keybindings::KeyPreset::Emacs),
|
keymap: crate::keybindings::KeyMap::new(crate::keybindings::KeyPreset::Emacs),
|
||||||
command_palette: crate::command_palette::CommandPalette::new(),
|
command_palette: crate::command_palette::CommandPalette::new(),
|
||||||
|
chat_mode: crate::chat_mode::ChatMode::Code,
|
||||||
needs_redraw: true,
|
needs_redraw: true,
|
||||||
};
|
};
|
||||||
me.draw_screen()?;
|
me.draw_screen()?;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue