Skip to main content

kaish_help/
topic.rs

1//! Topic compatibility surface for kaish help.
2//!
3//! Backs the `help <topic>` builtin and the MCP prompts: topic-based whole-document
4//! help embedded at compile time, plus dynamic tool help from the tool registry.
5//! Behavior here is intentionally byte-stable — frontends and tests depend on it.
6
7use kaish_types::ToolSchema;
8
9use crate::content::{IGNORE, LIMITS, OUTPUT_LIMIT, OVERLAY, OVERVIEW, SCATTER, SYNTAX, VFS};
10
11/// Help topics available in kaish.
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub enum HelpTopic {
14    /// Overview of kaish with topic list.
15    Overview,
16    /// Syntax reference: variables, quoting, pipes, control flow.
17    Syntax,
18    /// List of all available builtins.
19    Builtins,
20    /// Virtual filesystem mounts and paths.
21    Vfs,
22    /// Scatter/gather parallel processing.
23    Scatter,
24    /// Ignore file configuration.
25    Ignore,
26    /// Output size limit configuration.
27    OutputLimit,
28    /// Known limitations.
29    Limits,
30    /// Overlay VFS mode and kaish-vfs builtin.
31    Overlay,
32    /// Help for a specific tool.
33    Tool(String),
34}
35
36impl HelpTopic {
37    /// Parse a topic string into a HelpTopic.
38    ///
39    /// Returns Overview for empty/None, specific topics for known names,
40    /// or Tool(name) for anything else (assumes it's a tool name).
41    pub fn parse_topic(s: &str) -> Self {
42        match s.to_lowercase().as_str() {
43            "" | "overview" | "help" => Self::Overview,
44            "syntax" | "language" | "lang" => Self::Syntax,
45            "builtins" | "tools" | "commands" => Self::Builtins,
46            "vfs" | "filesystem" | "fs" | "paths" => Self::Vfs,
47            "scatter" | "gather" | "parallel" | "散" | "集" => Self::Scatter,
48            "ignore" | "gitignore" | "kaish-ignore" => Self::Ignore,
49            "output-limit" | "spill" | "truncate" | "kaish-output-limit" => Self::OutputLimit,
50            "limits" | "limitations" | "missing" => Self::Limits,
51            "overlay" | "kaish-vfs" | "vfs-overlay" => Self::Overlay,
52            other => Self::Tool(other.to_string()),
53        }
54    }
55
56    /// Get a short description of this topic.
57    pub fn description(&self) -> &'static str {
58        match self {
59            Self::Overview => "What kaish is, list of topics",
60            Self::Syntax => "Variables, quoting, pipes, control flow",
61            Self::Builtins => "List of available builtins",
62            Self::Vfs => "Virtual filesystem mounts and paths",
63            Self::Scatter => "Parallel processing (散/集)",
64            Self::Ignore => "Ignore file configuration",
65            Self::OutputLimit => "Output size limit configuration",
66            Self::Limits => "Known limitations",
67            Self::Overlay => "Copy-on-write overlay mode and kaish-vfs",
68            Self::Tool(_) => "Help for a specific tool",
69        }
70    }
71}
72
73/// Get help content for a topic.
74///
75/// For static topics, returns embedded markdown.
76/// For `Builtins`, generates a tool list from the provided schemas.
77/// For `Tool(name)`, looks up the tool in the schemas.
78pub fn get_help(topic: &HelpTopic, tool_schemas: &[ToolSchema]) -> String {
79    match topic {
80        HelpTopic::Overview => OVERVIEW.to_string(),
81        HelpTopic::Syntax => SYNTAX.to_string(),
82        HelpTopic::Builtins => format_tool_list(tool_schemas),
83        HelpTopic::Vfs => VFS.to_string(),
84        HelpTopic::Scatter => SCATTER.to_string(),
85        HelpTopic::Ignore => IGNORE.to_string(),
86        HelpTopic::OutputLimit => OUTPUT_LIMIT.to_string(),
87        HelpTopic::Limits => LIMITS.to_string(),
88        HelpTopic::Overlay => OVERLAY.to_string(),
89        HelpTopic::Tool(name) => format_tool_help(name, tool_schemas),
90    }
91}
92
93/// Format help for a single tool, or `None` if no such tool is registered.
94///
95/// The composition surface uses this; the `Unknown topic…` fallback lives in
96/// [`format_tool_help`] for the `help <topic>` command path.
97pub fn tool_help(name: &str, schemas: &[ToolSchema]) -> Option<String> {
98    let schema = schemas.iter().find(|s| s.name == name)?;
99    let mut output = String::new();
100
101    output.push_str(&format!("{} — {}\n\n", schema.name, schema.description));
102
103    if schema.params.is_empty() {
104        output.push_str("No parameters.\n");
105    } else {
106        output.push_str("Parameters:\n");
107        for param in &schema.params {
108            let req = if param.required { " (required)" } else { "" };
109            output.push_str(&format!(
110                "  {} : {}{}\n    {}\n",
111                param.name, param.param_type, req, param.description
112            ));
113        }
114    }
115
116    if !schema.examples.is_empty() {
117        output.push_str("\nExamples:\n");
118        for example in &schema.examples {
119            output.push_str(&format!("  # {}\n", example.description));
120            output.push_str(&format!("  {}\n\n", example.code));
121        }
122    }
123
124    Some(output)
125}
126
127/// Format help for a single tool.
128fn format_tool_help(name: &str, schemas: &[ToolSchema]) -> String {
129    tool_help(name, schemas).unwrap_or_else(|| {
130        format!(
131            "Unknown topic or tool: {}\n\nUse 'help' to see available topics, or 'help builtins' for tool list.",
132            name
133        )
134    })
135}
136
137/// Format a flat alphabetical list of all available tools.
138///
139/// Schemas arrive sorted from the registry; only registered tools appear,
140/// so feature-gated or unloaded builtins are omitted naturally.
141fn format_tool_list(schemas: &[ToolSchema]) -> String {
142    let mut output = String::from("# Available Builtins\n\n");
143
144    let max_len = schemas.iter().map(|s| s.name.len()).max().unwrap_or(0);
145
146    for schema in schemas {
147        output.push_str(&format!(
148            "  {:width$}  {}\n",
149            schema.name,
150            schema.description,
151            width = max_len
152        ));
153    }
154
155    output.push_str("\n---\n");
156    output.push_str("Use 'help <tool>' for detailed help on a specific tool.\n");
157    output.push_str("Use 'help syntax' for language syntax reference.\n");
158
159    output
160}
161
162/// List available help topics (for autocomplete, etc.).
163pub fn list_topics() -> Vec<(&'static str, &'static str)> {
164    vec![
165        ("overview", "What kaish is, list of topics"),
166        ("syntax", "Variables, quoting, pipes, control flow"),
167        ("builtins", "List of available builtins"),
168        ("vfs", "Virtual filesystem mounts and paths"),
169        ("scatter", "Parallel processing (散/集)"),
170        ("ignore", "Ignore file configuration"),
171        ("output-limit", "Output size limit configuration"),
172        ("limits", "Known limitations"),
173        ("overlay", "Copy-on-write overlay mode and kaish-vfs"),
174    ]
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_topic_parsing() {
183        assert_eq!(HelpTopic::parse_topic(""), HelpTopic::Overview);
184        assert_eq!(HelpTopic::parse_topic("overview"), HelpTopic::Overview);
185        assert_eq!(HelpTopic::parse_topic("syntax"), HelpTopic::Syntax);
186        assert_eq!(HelpTopic::parse_topic("SYNTAX"), HelpTopic::Syntax);
187        assert_eq!(HelpTopic::parse_topic("builtins"), HelpTopic::Builtins);
188        assert_eq!(HelpTopic::parse_topic("vfs"), HelpTopic::Vfs);
189        assert_eq!(HelpTopic::parse_topic("scatter"), HelpTopic::Scatter);
190        assert_eq!(HelpTopic::parse_topic("集"), HelpTopic::Scatter);
191        assert_eq!(HelpTopic::parse_topic("output-limit"), HelpTopic::OutputLimit);
192        assert_eq!(HelpTopic::parse_topic("spill"), HelpTopic::OutputLimit);
193        assert_eq!(HelpTopic::parse_topic("kaish-output-limit"), HelpTopic::OutputLimit);
194        assert_eq!(HelpTopic::parse_topic("limits"), HelpTopic::Limits);
195        assert_eq!(
196            HelpTopic::parse_topic("grep"),
197            HelpTopic::Tool("grep".to_string())
198        );
199    }
200
201    #[test]
202    fn test_static_content_embedded() {
203        // Verify the markdown files are embedded
204        assert!(OVERVIEW.contains("kaish"));
205        assert!(SYNTAX.contains("Variables"));
206        assert!(VFS.contains("Mount Points"));
207        assert!(SCATTER.contains("scatter"));
208        assert!(IGNORE.contains("kaish-ignore"));
209        assert!(OUTPUT_LIMIT.contains("kaish-output-limit"));
210        assert!(LIMITS.contains("Limitations"));
211    }
212
213    #[test]
214    fn test_get_help_overview() {
215        let content = get_help(&HelpTopic::Overview, &[]);
216        assert!(content.contains("kaish"));
217        assert!(content.contains("help syntax"));
218    }
219
220    #[test]
221    fn test_get_help_unknown_tool() {
222        let content = get_help(&HelpTopic::Tool("nonexistent".to_string()), &[]);
223        assert!(content.contains("Unknown topic or tool"));
224    }
225
226    #[test]
227    fn test_tool_help_none_for_missing() {
228        assert!(tool_help("nonexistent", &[]).is_none());
229    }
230}