codewhale-tui 0.8.59

Terminal UI for open-source and open-weight coding models
//! Core command area: model/provider selection, help, navigation, and the
//! persistent RLM / sub-agent entry points.

mod anchor;
#[allow(clippy::module_inception)]
mod core;
mod feedback;
mod hf;
mod hooks;
mod provider;
mod queue;
mod stash;
pub mod voice;

pub(in crate::commands) use self::core::reset_conversation_state;

use crate::commands::CommandResult;
use crate::commands::traits::{Command, CommandGroup, CommandInfo, FunctionCommand};
use crate::localization::MessageId;
use crate::tui::app::{App, AppAction};

pub struct CoreCommands;

impl CommandGroup for CoreCommands {
    fn commands(&self) -> Vec<Box<dyn Command>> {
        vec![
            Box::new(FunctionCommand::new(&ANCHOR_INFO, run_anchor)),
            Box::new(FunctionCommand::new(&HELP_INFO, run_help)),
            Box::new(FunctionCommand::new(&CLEAR_INFO, run_clear)),
            Box::new(FunctionCommand::new(&EXIT_INFO, run_exit)),
            Box::new(FunctionCommand::new(&MODEL_INFO, run_model)),
            Box::new(FunctionCommand::new(&MODELS_INFO, run_models)),
            Box::new(FunctionCommand::new(&PROVIDER_INFO, run_provider)),
            Box::new(FunctionCommand::new(&QUEUE_INFO, run_queue)),
            Box::new(FunctionCommand::new(&STASH_INFO, run_stash)),
            Box::new(FunctionCommand::new(&HOOKS_INFO, run_hooks)),
            Box::new(FunctionCommand::new(&SUBAGENTS_INFO, run_subagents)),
            Box::new(FunctionCommand::new(&AGENT_INFO, run_agent)),
            Box::new(FunctionCommand::new(&LINKS_INFO, run_links)),
            Box::new(FunctionCommand::new(&FEEDBACK_INFO, run_feedback)),
            Box::new(FunctionCommand::new(&HF_INFO, run_hf)),
            Box::new(FunctionCommand::new(&HOME_INFO, run_home)),
            Box::new(FunctionCommand::new(&WORKSPACE_INFO, run_workspace)),
            Box::new(FunctionCommand::new(&PROFILE_INFO, run_profile)),
            Box::new(FunctionCommand::new(&RLM_INFO, run_rlm)),
            Box::new(FunctionCommand::new(&TRANSLATE_INFO, run_translate)),
            Box::new(FunctionCommand::new(&VOICE_INFO, run_voice)),
            Box::new(FunctionCommand::new(&VOICE_SEND_INFO, run_voice_send)),
            Box::new(FunctionCommand::new(&VOICE_CONTROL_INFO, run_voice_control)),
        ]
    }
}

static ANCHOR_INFO: CommandInfo = CommandInfo {
    name: "anchor",
    aliases: &["maodian"],
    usage: "/anchor <text> | /anchor list | /anchor remove <n>",
    description_id: MessageId::CmdAnchorDescription,
};
static HELP_INFO: CommandInfo = CommandInfo {
    name: "help",
    aliases: &["?", "bangzhu", "帮助"],
    usage: "/help [command]",
    description_id: MessageId::CmdHelpDescription,
};
static CLEAR_INFO: CommandInfo = CommandInfo {
    name: "clear",
    aliases: &["qingping"],
    usage: "/clear",
    description_id: MessageId::CmdClearDescription,
};
static EXIT_INFO: CommandInfo = CommandInfo {
    name: "exit",
    aliases: &["quit", "q", "tuichu"],
    usage: "/exit",
    description_id: MessageId::CmdExitDescription,
};
static MODEL_INFO: CommandInfo = CommandInfo {
    name: "model",
    aliases: &["moxing"],
    usage: "/model [name]",
    description_id: MessageId::CmdModelDescription,
};
static MODELS_INFO: CommandInfo = CommandInfo {
    name: "models",
    aliases: &["moxingliebiao"],
    usage: "/models",
    description_id: MessageId::CmdModelsDescription,
};
static PROVIDER_INFO: CommandInfo = CommandInfo {
    name: "provider",
    aliases: &[],
    usage: "/provider [name] [model]",
    description_id: MessageId::CmdProviderDescription,
};
static QUEUE_INFO: CommandInfo = CommandInfo {
    name: "queue",
    aliases: &["queued"],
    usage: "/queue [list|edit <n>|drop <n>|clear]",
    description_id: MessageId::CmdQueueDescription,
};
static STASH_INFO: CommandInfo = CommandInfo {
    name: "stash",
    aliases: &["park"],
    usage: "/stash [list|pop|clear]",
    description_id: MessageId::CmdStashDescription,
};
static HOOKS_INFO: CommandInfo = CommandInfo {
    name: "hooks",
    aliases: &["hook", "gouzi"],
    usage: "/hooks [list|events]",
    description_id: MessageId::CmdHooksDescription,
};
static SUBAGENTS_INFO: CommandInfo = CommandInfo {
    name: "subagents",
    aliases: &["agents", "zhinengti"],
    usage: "/subagents",
    description_id: MessageId::CmdSubagentsDescription,
};
static AGENT_INFO: CommandInfo = CommandInfo {
    name: "agent",
    aliases: &["daili"],
    usage: "/agent [N] <task>",
    description_id: MessageId::CmdAgentDescription,
};
static LINKS_INFO: CommandInfo = CommandInfo {
    name: "links",
    aliases: &["dashboard", "api", "lianjie"],
    usage: "/links",
    description_id: MessageId::CmdLinksDescription,
};
static FEEDBACK_INFO: CommandInfo = CommandInfo {
    name: "feedback",
    aliases: &[],
    usage: "/feedback [bug|feature|security]",
    description_id: MessageId::CmdFeedbackDescription,
};
static HF_INFO: CommandInfo = CommandInfo {
    name: "hf",
    aliases: &["huggingface"],
    usage: "/hf [mcp <status|setup>|concepts]",
    description_id: MessageId::CmdHfDescription,
};
static HOME_INFO: CommandInfo = CommandInfo {
    name: "home",
    aliases: &["stats", "overview", "zhuye", "shouye"],
    usage: "/home",
    description_id: MessageId::CmdHomeDescription,
};
static WORKSPACE_INFO: CommandInfo = CommandInfo {
    name: "workspace",
    aliases: &["cwd"],
    usage: "/workspace [path]",
    description_id: MessageId::CmdWorkspaceDescription,
};
static PROFILE_INFO: CommandInfo = CommandInfo {
    name: "profile",
    aliases: &["dangan"],
    usage: "/profile <name>",
    description_id: MessageId::CmdHelpDescription,
};
static RLM_INFO: CommandInfo = CommandInfo {
    name: "rlm",
    aliases: &["recursive", "digui"],
    usage: "/rlm [N] <file_or_text>",
    description_id: MessageId::CmdRlmDescription,
};
static TRANSLATE_INFO: CommandInfo = CommandInfo {
    name: "translate",
    aliases: &["translation", "transale"],
    usage: "/translate",
    description_id: MessageId::CmdTranslateDescription,
};
static VOICE_INFO: CommandInfo = CommandInfo {
    name: "voice",
    aliases: &["yuyin", "语音"],
    usage: "/voice",
    description_id: MessageId::CmdVoiceDescription,
};
static VOICE_SEND_INFO: CommandInfo = CommandInfo {
    name: "voicesend",
    aliases: &["voice-send", "yuyinsend", "语音发送"],
    usage: "/voicesend",
    description_id: MessageId::CmdVoiceSendDescription,
};
static VOICE_CONTROL_INFO: CommandInfo = CommandInfo {
    name: "voicecontrol",
    aliases: &["voice-control", "yuyincontrol", "语音控制"],
    usage: "/voicecontrol",
    description_id: MessageId::CmdVoiceControlDescription,
};

fn run_registered(app: &mut App, name: &str, arg: Option<&str>) -> CommandResult {
    dispatch(app, name, arg).expect("registered core command should dispatch")
}

fn run_anchor(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "anchor", arg)
}
fn run_help(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "help", arg)
}
fn run_clear(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "clear", arg)
}
fn run_exit(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "exit", arg)
}
fn run_model(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "model", arg)
}
fn run_models(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "models", arg)
}
fn run_provider(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "provider", arg)
}
fn run_queue(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "queue", arg)
}
fn run_stash(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "stash", arg)
}
fn run_hooks(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "hooks", arg)
}
fn run_subagents(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "subagents", arg)
}
fn run_agent(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "agent", arg)
}
fn run_links(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "links", arg)
}
fn run_feedback(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "feedback", arg)
}
fn run_hf(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "hf", arg)
}
fn run_home(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "home", arg)
}
fn run_workspace(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "workspace", arg)
}
fn run_profile(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "profile", arg)
}
fn run_rlm(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "rlm", arg)
}
fn run_translate(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "translate", arg)
}
fn run_voice(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "voice", arg)
}
fn run_voice_send(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "voicesend", arg)
}
fn run_voice_control(app: &mut App, arg: Option<&str>) -> CommandResult {
    run_registered(app, "voicecontrol", arg)
}

pub(in crate::commands) fn dispatch(
    app: &mut App,
    command: &str,
    arg: Option<&str>,
) -> Option<CommandResult> {
    let result = match command {
        "anchor" | "maodian" => anchor::anchor(app, arg),
        "help" | "?" | "bangzhu" | "帮助" => core::help(app, arg),
        "clear" | "qingping" => core::clear(app),
        "exit" | "quit" | "q" | "tuichu" => core::exit(),
        "model" | "moxing" => core::model(app, arg),
        "models" | "moxingliebiao" => core::models(app),
        "provider" => provider::provider(app, arg),
        "queue" | "queued" => queue::queue(app, arg),
        "stash" | "park" => stash::stash(app, arg),
        "hooks" | "hook" | "gouzi" => hooks::hooks(app, arg),
        "subagents" | "agents" | "zhinengti" => core::subagents(app),
        "agent" | "daili" => agent(app, arg),
        "links" | "dashboard" | "api" | "lianjie" => core::deepseek_links(app),
        "feedback" => feedback::feedback(app, arg),
        "hf" | "huggingface" => hf::hf(app, arg),
        "home" | "stats" | "overview" | "zhuye" | "shouye" => core::home_dashboard(app),
        "workspace" | "cwd" => core::workspace_switch(app, arg),
        "profile" | "dangan" => core::profile_switch(app, arg),
        "rlm" | "recursive" | "digui" => rlm(app, arg),
        "translate" | "translation" | "transale" => core::translate(app),
        "voice" | "yuyin" | "语音" => voice::voice(app),
        "voicesend" | "voice-send" | "yuyinsend" | "语音发送" => voice::voice_send(app),
        "voicecontrol" | "voice-control" | "yuyincontrol" | "语音控制" => {
            voice::voice_control(app)
        }
        _ => return None,
    };
    Some(result)
}

/// Execute a Recursive Language Model (RLM) turn — Algorithm 1 from
/// Zhang et al. (arXiv:2512.24601).
///
/// The user's prompt text is passed as the argument. It will be stored
/// in the REPL as the `PROMPT` variable. The root LLM will only see
/// metadata about the REPL state, never the prompt text directly.
pub fn rlm(app: &mut App, arg: Option<&str>) -> CommandResult {
    let (max_depth, target) = match parse_depth_prefixed_arg(arg, 1) {
        Ok(parsed) => parsed,
        Err(message) => return CommandResult::error(message),
    };
    let target = match target {
        Some(p) if !p.trim().is_empty() => p.trim().to_string(),
        _ => {
            return CommandResult::error(
                "Usage: /rlm [N] <file_or_text>\n\n\
                 Opens a persistent RLM context with sub_rlm depth N (0-3, default 1)."
                    .to_string(),
            );
        }
    };

    let source_arg = if resolves_to_existing_file(app, &target) {
        format!(r#"file_path: "{target}""#)
    } else {
        format!("content: {target:?}")
    };
    let message = format!(
        "Open and use a persistent RLM session for this request. Call `rlm_open` with name `slash_rlm` and {source_arg}. Then call `rlm_configure` with `sub_rlm_max_depth: {max_depth}`. Use `rlm_eval` to inspect the context through `peek`, `search`, and `chunk`, and call `finalize(...)` from the REPL when ready. If a `var_handle` is returned, use `handle_read` for bounded slices or projections before answering."
    );

    CommandResult::with_message_and_action(
        format!("Opening persistent RLM context at depth {max_depth}..."),
        AppAction::SendMessage(message),
    )
}

/// Open a persistent sub-agent session from a slash command.
pub fn agent(_app: &mut App, arg: Option<&str>) -> CommandResult {
    let (max_depth, task) = match parse_depth_prefixed_arg(arg, 1) {
        Ok(parsed) => parsed,
        Err(message) => return CommandResult::error(message),
    };
    let task = match task {
        Some(task) if !task.trim().is_empty() => task.trim().to_string(),
        _ => {
            return CommandResult::error(
                "Usage: /agent [N] <task>\n\n\
                 Opens a persistent sub-agent session with recursive agent depth N (0-3, default 1).",
            );
        }
    };
    let message = format!(
        "Open a persistent sub-agent session for this task. Call `agent_open` with name `slash_agent`, `prompt: {task:?}`, and `max_depth: {max_depth}`. Use `agent_eval` to wait for the next terminal/current projection and `handle_read` on the returned transcript_handle if you need more detail. Verify any claimed side effects before reporting success."
    );
    CommandResult::with_message_and_action(
        format!("Opening persistent sub-agent at depth {max_depth}..."),
        AppAction::SendMessage(message),
    )
}

fn parse_depth_prefixed_arg(
    arg: Option<&str>,
    default_depth: u32,
) -> Result<(u32, Option<&str>), String> {
    let Some(raw) = arg.map(str::trim).filter(|raw| !raw.is_empty()) else {
        return Ok((default_depth, None));
    };
    let mut parts = raw.splitn(2, char::is_whitespace);
    let first = parts.next().unwrap_or_default();
    if first.chars().all(|ch| ch.is_ascii_digit()) {
        let depth: u32 = first
            .parse()
            .map_err(|_| "Depth must be an integer from 0 to 3".to_string())?;
        if depth > 3 {
            return Err("Depth must be between 0 and 3".to_string());
        }
        Ok((depth, parts.next().map(str::trim)))
    } else {
        Ok((default_depth, Some(raw)))
    }
}

fn resolves_to_existing_file(app: &App, input: &str) -> bool {
    let path = std::path::Path::new(input);
    let candidate = if path.is_absolute() {
        path.to_path_buf()
    } else {
        app.workspace.join(path)
    };
    candidate.is_file()
}