/ archive / python-cli-final / kamaji / autocomplete.py
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