Skip to main content

zeph_commands/handlers/
help.rs

1// SPDX-FileCopyrightText: 2026 Andrei G <bug-ops>
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! `/help` command handler.
5
6use std::fmt::Write as _;
7use std::future::Future;
8use std::pin::Pin;
9
10use crate::context::CommandContext;
11use crate::{CommandError, CommandHandler, CommandOutput, SlashCategory};
12
13/// Render the full `/help` listing: every entry in [`crate::COMMANDS`], grouped by
14/// [`SlashCategory`] in display order.
15///
16/// Extracted from [`HelpCommand::handle`] so callers without a [`CommandContext`] — notably
17/// the ACP session path (`zeph-acp`'s `is_acp_native_slash_command` shortcut, #5986) — can
18/// render the same command listing the CLI/TUI's `CommandRegistry` produces, instead of
19/// hand-rolling a separate, drift-prone copy.
20#[must_use]
21pub fn render_help_text() -> String {
22    let mut out = String::from("Slash commands:\n\n");
23
24    let categories = [
25        SlashCategory::Session,
26        SlashCategory::Configuration,
27        SlashCategory::Memory,
28        SlashCategory::Skills,
29        SlashCategory::Planning,
30        SlashCategory::Integration,
31        SlashCategory::Debugging,
32        SlashCategory::Advanced,
33    ];
34
35    for cat in &categories {
36        let entries: Vec<_> = crate::COMMANDS
37            .iter()
38            .filter(|c| &c.category == cat)
39            .collect();
40        if entries.is_empty() {
41            continue;
42        }
43        let _ = writeln!(out, "{}:", cat.as_str());
44        for cmd in entries {
45            if cmd.args.is_empty() {
46                let _ = write!(out, "  {}", cmd.name);
47            } else {
48                let _ = write!(out, "  {} {}", cmd.name, cmd.args);
49            }
50            let _ = write!(out, "  — {}", cmd.description);
51            if let Some(feat) = cmd.feature_gate {
52                let _ = write!(out, " [requires: {feat}]");
53            }
54            let _ = writeln!(out);
55        }
56        let _ = writeln!(out);
57    }
58
59    out.trim_end().to_owned()
60}
61
62/// Display all available slash commands grouped by category.
63pub struct HelpCommand;
64
65impl CommandHandler<CommandContext<'_>> for HelpCommand {
66    fn name(&self) -> &'static str {
67        "/help"
68    }
69
70    fn description(&self) -> &'static str {
71        "Show this help message"
72    }
73
74    fn category(&self) -> SlashCategory {
75        SlashCategory::Debugging
76    }
77
78    fn requires_auth(&self) -> bool {
79        false
80    }
81
82    fn handle<'a>(
83        &'a self,
84        _ctx: &'a mut CommandContext<'_>,
85        _args: &'a str,
86    ) -> Pin<Box<dyn Future<Output = Result<CommandOutput, CommandError>> + Send + 'a>> {
87        use tracing::Instrument as _;
88        let span = tracing::info_span!("commands.help.handle");
89        Box::pin(async move { Ok(CommandOutput::Message(render_help_text())) }.instrument(span))
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96    use crate::handlers::test_helpers::{MockDebug, MockMessages, MockSession, make_ctx};
97    use crate::sink::NullSink;
98
99    #[test]
100    fn help_name_and_description() {
101        assert_eq!(HelpCommand.name(), "/help");
102        assert!(!HelpCommand.description().is_empty());
103    }
104
105    #[test]
106    fn help_requires_auth_false() {
107        assert!(!HelpCommand.requires_auth());
108    }
109
110    #[tokio::test]
111    async fn help_returns_message_with_slash_commands_header() {
112        let mut sink = NullSink;
113        let mut debug = MockDebug;
114        let mut messages = MockMessages;
115        let session = MockSession;
116        let mut agent = crate::NullAgent;
117        let mut ctx = make_ctx(&mut sink, &mut debug, &mut messages, &session, &mut agent);
118        let out = HelpCommand.handle(&mut ctx, "").await.unwrap();
119        let CommandOutput::Message(msg) = out else {
120            panic!("expected Message")
121        };
122        assert!(msg.contains("Slash commands"));
123    }
124}