Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion cortex-tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1248,7 +1248,8 @@ impl AppState {
// Clear any partial text segment that might have incomplete line wrapping
// The typewriter will regenerate content on the next render
if let Some(ref mut tw) = self.typewriter {
tw.reset_animation();
// Reset the typewriter text - it will be regenerated on next render
tw.set_text(String::new());
}

// Re-pin to bottom if we were following the stream
Expand Down
274 changes: 274 additions & 0 deletions cortex-tui/src/commands/executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,18 @@ impl CommandExecutor {
.to_string(),
),

// Direct clipboard paste (#3087)
"paste" | "pb" => CommandResult::Async("clipboard:paste".to_string()),

// Prompt macros (#3085)
"macro" | "macros" => self.cmd_macro(cmd),

// Configuration validation (#3090)
"validate-config" | "check-config" => self.cmd_validate_config(cmd),

// External editor (#3086)
"editor" | "edit" | "vi" => CommandResult::Async("editor:open".to_string()),

// ============ AUTH ============
"login" | "signin" => CommandResult::OpenModal(ModalType::Login),
"logout" | "signout" => CommandResult::Async("auth:logout".to_string()),
Expand Down Expand Up @@ -125,6 +137,12 @@ impl CommandExecutor {
"rewind" | "rw" => self.cmd_rewind(cmd),
"delete" | "rm" => self.cmd_delete(cmd),

// Session branching from history (#3081)
"branch-at" | "fork-at" => self.cmd_branch_at(cmd),

// Edit previous message (#3088)
"edit-message" | "edit-msg" | "em" => self.cmd_edit_message(cmd),

// ============ NAVIGATION ============
"diff" | "d" => self.cmd_diff(cmd),
"transcript" | "tr" => CommandResult::Async("transcript".to_string()),
Expand Down Expand Up @@ -632,6 +650,108 @@ impl CommandExecutor {
CommandResult::Async(format!("eval:{}", cmd.args_string()))
}
}

// ========== NEW COMMANDS (#3081-3090) ==========

/// Handles the /branch-at command to create a branch from a specific message in history.
/// (#3081 Session Branching from History Points)
///
/// Supports:
/// - `/branch-at <index>` - Branch from message at index (1-based)
fn cmd_branch_at(&self, cmd: &ParsedCommand) -> CommandResult {
match cmd.first_arg() {
Some(index) => {
if let Ok(n) = index.parse::<usize>() {
if n == 0 {
return CommandResult::Error(
"Message index must be 1 or greater.".to_string(),
);
}
CommandResult::Async(format!("session:branch-at:{}", n))
} else {
CommandResult::Error(format!("Invalid message index: {}. Use a number.", index))
}
}
None => CommandResult::Error(
"Usage: /branch-at <message-index>\nExample: /branch-at 5".to_string(),
),
}
}

/// Handles the /edit-message command to edit and resend a previous message.
/// (#3088 Message Editing in Interactive Mode)
///
/// Supports:
/// - `/edit-message` - Edit the last user message
/// - `/edit-message <index>` - Edit message at specific index
fn cmd_edit_message(&self, cmd: &ParsedCommand) -> CommandResult {
match cmd.first_arg() {
Some(index) => {
if let Ok(n) = index.parse::<usize>() {
CommandResult::Async(format!("message:edit:{}", n))
} else {
CommandResult::Error(format!("Invalid message index: {}", index))
}
}
// Default to editing the last user message
None => CommandResult::Async("message:edit:last".to_string()),
}
}

/// Handles the /macro command for prompt macros.
/// (#3085 Prompt Macros for Reusable Text)
///
/// Supports:
/// - `/macro list` - List all saved macros
/// - `/macro save <name> <content>` - Save a new macro
/// - `/macro delete <name>` - Delete a macro
/// - `/macro use <name>` - Insert macro content into input
/// - `/macro <name>` - Shorthand for use
fn cmd_macro(&self, cmd: &ParsedCommand) -> CommandResult {
match cmd.first_arg() {
Some("list") | Some("ls") => CommandResult::Async("macro:list".to_string()),
Some("save") | Some("add") => {
if cmd.args.len() < 3 {
return CommandResult::Error("Usage: /macro save <name> <content>".to_string());
}
let name = &cmd.args[1];
let content = cmd.args[2..].join(" ");
CommandResult::Async(format!("macro:save:{}:{}", name, content))
}
Some("delete") | Some("rm") => {
if cmd.args.len() < 2 {
return CommandResult::Error("Usage: /macro delete <name>".to_string());
}
let name = &cmd.args[1];
CommandResult::Async(format!("macro:delete:{}", name))
}
Some("use") => {
if cmd.args.len() < 2 {
return CommandResult::Error("Usage: /macro use <name>".to_string());
}
let name = &cmd.args[1];
CommandResult::Async(format!("macro:use:{}", name))
}
Some(name) => {
// Shorthand: /macro <name> is equivalent to /macro use <name>
CommandResult::Async(format!("macro:use:{}", name))
}
None => CommandResult::Async("macro:list".to_string()),
}
}

/// Handles the /validate-config command for configuration validation.
/// (#3090 Configuration Validation Command)
///
/// Supports:
/// - `/validate-config` - Validate default config
/// - `/validate-config <path>` - Validate specific config file
fn cmd_validate_config(&self, cmd: &ParsedCommand) -> CommandResult {
match cmd.first_arg() {
Some(path) => CommandResult::Async(format!("config:validate:{}", path)),
None => CommandResult::Async("config:validate".to_string()),
}
}
}

impl Default for CommandExecutor {
Expand Down Expand Up @@ -1145,4 +1265,158 @@ mod tests {
CommandResult::Async(ref s) if s == "share:7d"
));
}

// ========== NEW COMMANDS TESTS (#3081-3090) ==========

#[test]
fn test_branch_at_command() {
let executor = CommandExecutor::new();

// With valid index
let result = executor.execute_str("/branch-at 5");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "session:branch-at:5"
));

// Without argument
let result = executor.execute_str("/branch-at");
assert!(matches!(result, CommandResult::Error(_)));

// With invalid index
let result = executor.execute_str("/branch-at abc");
assert!(matches!(result, CommandResult::Error(_)));

// With zero index
let result = executor.execute_str("/branch-at 0");
assert!(matches!(result, CommandResult::Error(_)));
}

#[test]
fn test_edit_message_command() {
let executor = CommandExecutor::new();

// Without argument - edit last
let result = executor.execute_str("/edit-message");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "message:edit:last"
));

// With index
let result = executor.execute_str("/edit-message 3");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "message:edit:3"
));

// Alias
let result = executor.execute_str("/em 2");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "message:edit:2"
));
}

#[test]
fn test_macro_command() {
let executor = CommandExecutor::new();

// List macros
let result = executor.execute_str("/macro list");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "macro:list"
));

// Save macro
let result = executor.execute_str("/macro save greeting Hello, world!");
assert!(matches!(
result,
CommandResult::Async(ref s) if s.starts_with("macro:save:greeting:")
));

// Delete macro
let result = executor.execute_str("/macro delete greeting");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "macro:delete:greeting"
));

// Use macro
let result = executor.execute_str("/macro use greeting");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "macro:use:greeting"
));

// Shorthand use
let result = executor.execute_str("/macro greeting");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "macro:use:greeting"
));

// No args - list
let result = executor.execute_str("/macro");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "macro:list"
));
}

#[test]
fn test_validate_config_command() {
let executor = CommandExecutor::new();

// Without path
let result = executor.execute_str("/validate-config");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "config:validate"
));

// With path
let result = executor.execute_str("/validate-config /path/to/config.toml");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "config:validate:/path/to/config.toml"
));

// Alias
let result = executor.execute_str("/check-config");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "config:validate"
));
}

#[test]
fn test_paste_command() {
let executor = CommandExecutor::new();

let result = executor.execute_str("/paste");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "clipboard:paste"
));
}

#[test]
fn test_editor_command() {
let executor = CommandExecutor::new();

let result = executor.execute_str("/editor");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "editor:open"
));

// Alias
let result = executor.execute_str("/vi");
assert!(matches!(
result,
CommandResult::Async(ref s) if s == "editor:open"
));
}
}
60 changes: 60 additions & 0 deletions cortex-tui/src/commands/registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,46 @@ pub fn register_builtin_commands(registry: &mut CommandRegistry) {
false,
));

// Direct clipboard paste command (#3087)
registry.register(CommandDef::new(
"paste",
&["pb"],
"Paste text from clipboard into input",
"/paste",
CommandCategory::General,
false,
));

// Prompt macros (#3085)
registry.register(CommandDef::new(
"macro",
&["macros"],
"Save or use prompt macros",
"/macro [save|list|delete|use] [name] [content]",
CommandCategory::General,
true,
));

// Configuration validation (#3090)
registry.register(CommandDef::new(
"validate-config",
&["check-config"],
"Validate configuration file without running other commands",
"/validate-config [config-path]",
CommandCategory::General,
true,
));

// Open external editor for long prompts (#3086)
registry.register(CommandDef::new(
"editor",
&["edit", "vi"],
"Open $EDITOR for composing long prompts",
"/editor",
CommandCategory::General,
false,
));

registry.register(CommandDef::new(
"theme",
&[],
Expand Down Expand Up @@ -489,6 +529,26 @@ pub fn register_builtin_commands(registry: &mut CommandRegistry) {
true,
));

// Branch from specific history point (#3081)
registry.register(CommandDef::new(
"branch-at",
&["fork-at"],
"Create branch from specific message in history",
"/branch-at <message-index>",
CommandCategory::Session,
true,
));

// Edit previous message (#3088)
registry.register(CommandDef::new(
"edit-message",
&["edit-msg", "em"],
"Edit and resend a previous message",
"/edit-message [message-index]",
CommandCategory::Session,
true,
));

// ========================================
// NAVIGATION COMMANDS
// ========================================
Expand Down
Loading