autocomplete.py
1 """ 2 Autocomplete and command suggestions for Kamaji TUI. 3 """ 4 5 from typing import List, Dict, Tuple, Optional 6 from pathlib import Path 7 8 9 class CommandAutocomplete: 10 """ 11 Provides autocomplete suggestions for commands and keywords. 12 """ 13 14 def __init__(self): 15 """Initialize the autocomplete system with built-in commands.""" 16 # Command definitions: command -> (description, category) 17 self.commands: Dict[str, Tuple[str, str]] = { 18 # Agent commands 19 "@review": ("Code review specialist agent", "agents"), 20 "@docs": ("Documentation specialist agent", "agents"), 21 "@help": ("Show available agents", "agents"), 22 23 # Meta commands 24 "explain": ("Show documentation for a topic", "meta"), 25 "clear": ("Clear conversation history", "meta"), 26 "mature": ("Analyze codebase and suggest improvements", "meta"), 27 "work": ("Work on tasks (internal list, prompt, or path)", "meta"), 28 "create agent": ("🎨 AI-powered custom agent builder", "meta"), 29 "agent info": ("🔍 View agent specifications and details", "meta"), 30 31 # Memory commands 32 "memorize": ("🧠 Store something in memory", "memory"), 33 "remember": ("🧠 Recall memories by keyword", "memory"), 34 "memory": ("🧠 Show all stored memories", "memory"), 35 36 # File commands 37 "/files": ("Show loaded files", "files"), 38 "/add": ("Add a file to context", "files"), 39 "/pwd": ("Show current directory", "files"), 40 "/ls": ("List files in current directory", "files"), 41 "/loadpy": ("Load all Python files", "files"), 42 43 # History commands 44 "/history": ("Show command history", "history"), 45 "/clearhistory": ("Clear command history", "history"), 46 } 47 48 # Explanation topics - maps user-friendly names to documentation files 49 self.explain_topics: Dict[str, str] = { 50 "agents": "AGENTS_GUIDE.md", 51 "architecture": "ARCHITECTURE.md", 52 "integration": "INTEGRATION_EXAMPLE.md", 53 "usage": "MULTI_AGENT_USAGE.md", 54 "multi agent": "MULTI_AGENT_USAGE.md", 55 "code review": "AGENTS_GUIDE.md", 56 "documentation agent": "AGENTS_GUIDE.md", 57 "routing": "AGENTS_GUIDE.md", 58 } 59 60 def get_suggestions(self, partial_input: str) -> List[Tuple[str, str]]: 61 """ 62 Get autocomplete suggestions for partial input. 63 64 Args: 65 partial_input: The text the user has typed so far 66 67 Returns: 68 List of (command, description) tuples that match the input 69 """ 70 if not partial_input: 71 return [] 72 73 partial_lower = partial_input.lower() 74 suggestions = [] 75 76 # Special case: "@" shows ALL available agents 77 if partial_lower == "@": 78 for command, (description, category) in self.commands.items(): 79 if category == "agents": 80 suggestions.append((command, description)) 81 suggestions.sort(key=lambda x: (len(x[0]), x[0])) 82 return suggestions 83 84 # Special case: "explain " shows documentation topics 85 if partial_lower.startswith("explain "): 86 topic_prefix = partial_lower[8:] # After "explain " 87 for topic in self.explain_topics.keys(): 88 if topic.lower().startswith(topic_prefix) or not topic_prefix: 89 suggestions.append((f"explain {topic}", f"Documentation: {topic}")) 90 suggestions.sort(key=lambda x: (len(x[0]), x[0])) 91 return suggestions 92 93 # Match commands 94 for command, (description, category) in self.commands.items(): 95 if command.lower().startswith(partial_lower): 96 suggestions.append((command, description)) 97 98 # Sort by length (shortest first) then alphabetically 99 suggestions.sort(key=lambda x: (len(x[0]), x[0])) 100 101 return suggestions 102 103 def get_best_match(self, partial_input: str) -> Optional[str]: 104 """ 105 Get the best autocomplete match (for grey preview). 106 107 Args: 108 partial_input: The text the user has typed so far 109 110 Returns: 111 The best matching command, or None if no match 112 """ 113 if not partial_input: 114 return None 115 116 # Special handling for @ - show first agent 117 if partial_input == "@": 118 # Show all available agents in grey 119 return "@help" 120 121 # Special handling for "explain " - show first topic 122 partial_lower = partial_input.lower() 123 if partial_lower == "explain ": 124 topics = sorted(self.explain_topics.keys()) 125 if topics: 126 return f"explain {topics[0]}" 127 128 suggestions = self.get_suggestions(partial_input) 129 if suggestions: 130 # Return the shortest match (most likely intended) 131 return suggestions[0][0] 132 133 return None 134 135 def get_completion(self, partial_input: str) -> str: 136 """ 137 Get the grey completion text (only the part to be added). 138 139 Args: 140 partial_input: The text the user has typed so far 141 142 Returns: 143 The text to show in grey after the cursor 144 """ 145 best_match = self.get_best_match(partial_input) 146 if best_match: 147 # Return only the part that's not yet typed 148 return best_match[len(partial_input):] 149 return "" 150 151 def get_explain_topics(self) -> List[str]: 152 """ 153 Get list of available explanation topics. 154 155 Returns: 156 List of topic names (formatted nicely) 157 """ 158 return sorted(set(self.explain_topics.keys())) 159 160 def get_doc_file(self, topic: str) -> Optional[Path]: 161 """ 162 Get the documentation file path for a topic. 163 164 Args: 165 topic: The topic to explain (case-insensitive) 166 167 Returns: 168 Path to the documentation file, or None if not found 169 """ 170 topic_lower = topic.lower().strip() 171 172 # Direct match 173 if topic_lower in self.explain_topics: 174 doc_file = self.explain_topics[topic_lower] 175 # Look for file in project root 176 for possible_path in [ 177 Path.cwd() / doc_file, 178 Path(__file__).parent.parent / doc_file, 179 ]: 180 if possible_path.exists(): 181 return possible_path 182 183 return None 184 185 def format_markdown_for_display(self, markdown_content: str) -> str: 186 """ 187 Format markdown content for readable display. 188 189 Converts: 190 - # Headers -> capitalized text 191 - **bold** -> UPPERCASE 192 - `code` -> 'code' 193 - Lists -> indented 194 - Remove excessive newlines 195 196 Args: 197 markdown_content: Raw markdown content 198 199 Returns: 200 Formatted, readable English text 201 """ 202 lines = markdown_content.split('\n') 203 formatted = [] 204 in_code_block = False 205 206 for line in lines: 207 stripped = line.strip() 208 209 # Skip empty lines (but keep one for paragraphs) 210 if not stripped: 211 if formatted and formatted[-1] != '': 212 formatted.append('') 213 continue 214 215 # Code blocks 216 if stripped.startswith('```'): 217 in_code_block = not in_code_block 218 continue 219 220 if in_code_block: 221 formatted.append(f" {line}") 222 continue 223 224 # Headers (convert to plain text, capitalized) 225 if stripped.startswith('#'): 226 header_text = stripped.lstrip('#').strip() 227 formatted.append('') 228 formatted.append(header_text.upper()) 229 formatted.append('=' * len(header_text)) 230 continue 231 232 # Bold text (convert to uppercase) 233 if '**' in stripped: 234 stripped = stripped.replace('**', '') 235 236 # Inline code (keep backticks as quotes) 237 if '`' in stripped: 238 stripped = stripped.replace('`', "'") 239 240 # Lists (indent) 241 if stripped.startswith(('-', '*', '•')): 242 formatted.append(f" {stripped}") 243 continue 244 245 # Numbered lists 246 if len(stripped) > 2 and stripped[0].isdigit() and stripped[1] in '.):': 247 formatted.append(f" {stripped}") 248 continue 249 250 # Regular paragraphs 251 formatted.append(stripped) 252 253 # Remove excessive blank lines 254 result = [] 255 prev_blank = False 256 for line in formatted: 257 if line == '': 258 if not prev_blank: 259 result.append(line) 260 prev_blank = True 261 else: 262 result.append(line) 263 prev_blank = False 264 265 return '\n'.join(result).strip() 266 267 268 def get_autocomplete() -> CommandAutocomplete: 269 """ 270 Get a singleton instance of CommandAutocomplete. 271 272 Returns: 273 CommandAutocomplete instance 274 """ 275 if not hasattr(get_autocomplete, '_instance'): 276 get_autocomplete._instance = CommandAutocomplete() 277 return get_autocomplete._instance