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