Coverage for src / osiris_cli / enhanced_cli.py: 0%
1065 statements
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-27 17:41 +0200
« prev ^ index » next coverage.py v7.13.0, created at 2025-12-27 17:41 +0200
1#!/usr/bin/env python3
2"""
3Osiris CLI - Enhanced Rich Interface with Interactive Menus
4Advanced terminal UI with menu navigation and multi-select capabilities
5"""
7from rich.console import Console
8from rich.panel import Panel
9from rich.table import Table
10from rich.prompt import Prompt, Confirm, IntPrompt
11from rich.live import Live
12from rich.columns import Columns
13from rich.align import Align
14from rich.text import Text
15from rich.spinner import Spinner
16from rich.markdown import Markdown
17from rich.layout import Layout
18from rich.padding import Padding
19from rich.box import ROUNDED, DOUBLE, HEAVY_HEAD, SIMPLE_HEAVY
20import asyncio
21import json
22import os
23import sys
24from pathlib import Path
25from typing import List, Dict, Any, Optional, Tuple
26import time
27import threading
28from contextlib import contextmanager
30from .config import settings
31from .enhanced_commands import enhanced_app
32from .client import async_client
33from .context import context
34from .session import session
35from .tools import tools
37console = Console()
39class ModernMenu:
40 """Modern, card-based menu system with dashboard-style layout"""
42 def __init__(self, title: str, sections: List[Dict[str, Any]]):
43 self.title = title
44 self.sections = sections
45 self.selected_section = 0
46 self.selected_item = 0
48 def display(self) -> str:
49 """Display modern dashboard-style menu interface"""
50 import sys
52 # Terminal detection
53 terminal_width = console.size.width if hasattr(console, 'size') else 80
54 terminal_height = console.size.height if hasattr(console, 'size') else 24
56 # Enable raw input mode
57 old_settings = None
58 raw_mode = False
59 try:
60 import termios
61 import tty
62 old_settings = termios.tcgetattr(sys.stdin)
63 tty.setraw(sys.stdin.fileno())
64 raw_mode = True
65 except:
66 raw_mode = False
68 try:
69 with Live(console=console, refresh_per_second=30, auto_refresh=False) as live:
70 while True:
71 # Create the main layout
72 layout_content = self._render_dashboard(terminal_width, terminal_height)
73 live.update(layout_content)
74 live.refresh()
76 # Handle input
77 if raw_mode:
78 try:
79 import select
80 ready, _, _ = select.select([sys.stdin], [], [], 0.1)
81 if ready:
82 char = sys.stdin.read(1)
84 if char == '\x1b': # Escape sequence
85 seq = sys.stdin.read(2)
86 if seq == '[A': # Up
87 self._navigate_up()
88 elif seq == '[B': # Down
89 self._navigate_down()
90 elif seq == '[D': # Left
91 self._navigate_left()
92 elif seq == '[C': # Right
93 self._navigate_right()
95 elif char == '\r' or char == '\n': # Enter
96 return self._get_selected_action()
97 elif char == ' ': # Space
98 return self._get_selected_action()
99 elif char in ['q', 'Q']:
100 return "cancel"
101 elif char.isdigit() and char != '0':
102 # Direct number selection
103 num = int(char)
104 if 1 <= num <= 9:
105 return self._get_action_by_number(num)
106 elif char in ['h', 'H', '?']: # Help
107 self._show_help(live)
108 except:
109 pass
110 else:
111 # Fallback input
112 try:
113 choice = input("Navigate (arrows/wasd/number/q): ").strip().lower()
114 if choice in ['q', 'quit', 'cancel']:
115 return "cancel"
116 elif choice.isdigit() and choice != '0':
117 num = int(choice)
118 if 1 <= num <= 9:
119 return self._get_action_by_number(num)
120 elif choice in ['w', 'up', 'k']:
121 self._navigate_up()
122 elif choice in ['s', 'down', 'j']:
123 self._navigate_down()
124 elif choice in ['a', 'left', 'h']:
125 self._navigate_left()
126 elif choice in ['d', 'right', 'l']:
127 self._navigate_right()
128 elif choice in ['', 'enter']:
129 return self._get_selected_action()
130 except (EOFError, KeyboardInterrupt):
131 return "cancel"
133 except KeyboardInterrupt:
134 return "cancel"
135 finally:
136 if raw_mode and old_settings:
137 try:
138 import termios
139 termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
140 except:
141 pass
143 def _render_dashboard(self, width: int, height: int) -> Layout:
144 """Render the modern dashboard layout"""
145 layout = Layout()
147 # Header section
148 header = self._render_header(width)
150 # Main content area
151 content = self._render_content(width, height)
153 # Footer with controls
154 footer = self._render_footer(width)
156 # Assemble layout
157 layout.split_column(
158 Layout(header, name="header", size=6),
159 Layout(content, name="content", size=height - 12),
160 Layout(footer, name="footer", size=4)
161 )
163 return layout
165 def _get_status_info(self) -> List[str]:
166 """Get status information for header"""
167 # This would normally get real status info
168 return ["Agent: Ready", "Tools: 6 Available"]
170 def _render_header(self, width: int) -> Panel:
171 """Render the header section"""
172 title = "OSIRIS COMMAND CENTER" if width > 50 else "OSIRIS CMD"
174 status_info = self._get_status_info()
176 header_content = f"[bold cyan]{title}[/bold cyan]\n"
177 header_content += f"[dim]{'─' * min(width-4, 60)}[/dim]\n\n"
179 if status_info:
180 for info in status_info[:2]: # Show max 2 status items
181 header_content += f"[white]{info}[/white]\n"
183 return Panel(
184 header_content,
185 border_style="cyan",
186 padding=(1, 2)
187 )
189 def _render_content(self, width: int, height: int) -> Layout:
190 """Render the main content area with sections"""
191 content_layout = Layout()
193 if width < 60:
194 # Single column layout for narrow screens
195 content_layout = self._render_single_column(width, height)
196 else:
197 # Multi-column layout for wider screens
198 content_layout = self._render_multi_column(width, height)
200 return content_layout
202 def _render_single_column(self, width: int, height: int) -> Layout:
203 """Render single column layout for narrow screens"""
204 layout = Layout()
206 sections_content = []
207 for i, section in enumerate(self.sections):
208 section_panel = self._render_section(section, i, width, is_selected=(i == self.selected_section))
209 sections_content.append(section_panel)
211 # Join sections vertically
212 content = Layout()
213 content.split_column(*[Layout(panel, size=max(8, height//len(self.sections))) for panel in sections_content])
215 return content
217 def _render_multi_column(self, width: int, height: int) -> Layout:
218 """Render multi-column layout for wide screens"""
219 layout = Layout()
221 # Group sections into columns (max 3 columns)
222 columns_data = []
223 sections_per_column = max(1, len(self.sections) // 3 + 1)
225 for col in range(3):
226 start_idx = col * sections_per_column
227 end_idx = min((col + 1) * sections_per_column, len(self.sections))
228 if start_idx < len(self.sections):
229 column_sections = self.sections[start_idx:end_idx]
230 column_content = []
231 for i, section in enumerate(column_sections):
232 global_idx = start_idx + i
233 section_panel = self._render_section(
234 section, global_idx, width//3,
235 is_selected=(global_idx == self.selected_section)
236 )
237 column_content.append(section_panel)
239 columns_data.append(column_content)
241 # Create column layouts
242 column_layouts = []
243 for col_data in columns_data:
244 col_layout = Layout()
245 col_layout.split_column(*[Layout(panel, size=max(8, height//len(col_data))) for panel in col_data])
246 column_layouts.append(col_layout)
248 # Combine columns
249 if len(column_layouts) == 1:
250 layout = column_layouts[0]
251 elif len(column_layouts) == 2:
252 layout.split_row(
253 Layout(column_layouts[0], name="left"),
254 Layout(column_layouts[1], name="right")
255 )
256 else:
257 layout.split_row(
258 Layout(column_layouts[0], name="left"),
259 Layout(column_layouts[1], name="center"),
260 Layout(column_layouts[2], name="right")
261 )
263 return layout
265 def _render_section(self, section: Dict[str, Any], index: int, width: int, is_selected: bool = False) -> Panel:
266 """Render a single section/card"""
267 title = section.get('title', 'Section')
268 items = section.get('items', [])
269 icon = section.get('icon', '📁')
271 # Section styling
272 border_style = "green" if is_selected else "blue"
273 title_style = "bold green" if is_selected else "bold white"
275 content = f"[{title_style}]{icon} {title}[/{title_style}]\n"
276 content += f"[dim]{'─' * min(width-6, 20)}[/dim]\n\n"
278 # Show items in the section
279 for i, item in enumerate(items[:4]): # Max 4 items per section
280 item_name = item.get('name', '')[:min(width//2, 15)]
281 if i == self.selected_item and is_selected:
282 content += f"[bold cyan]▶ {item_name}[/bold cyan]\n"
283 else:
284 content += f"[dim] {item_name}[/dim]\n"
286 # Show more indicator if there are more items
287 if len(items) > 4:
288 content += f"[dim]... +{len(items)-4} more[/dim]\n"
290 return Panel(
291 content,
292 border_style=border_style,
293 padding=(1, 1)
294 )
296 def _render_footer(self, width: int) -> Panel:
297 """Render the footer with controls"""
298 controls = []
299 if width > 50:
300 controls = [
301 "[bold white]↑↓←→[/bold white] Navigate",
302 "[bold white]Enter[/bold white] Select",
303 "[bold white]1-9[/bold white] Quick Select",
304 "[bold white]Q[/bold white] Quit",
305 "[bold white]?[/bold white] Help"
306 ]
307 else:
308 controls = [
309 "[bold white]↑↓[/bold white] Nav",
310 "[bold white]Enter[/bold white] Select",
311 "[bold white]1-9[/bold white] Quick",
312 "[bold white]Q[/bold white] Quit"
313 ]
315 footer_content = " | ".join(controls)
317 return Panel(
318 f"[dim]{footer_content}[/dim]",
319 border_style="dim blue",
320 padding=(0, 1)
321 )
323 def _navigate_up(self):
324 """Navigate up in current section"""
325 current_section = self.sections[self.selected_section]
326 items = current_section.get('items', [])
327 if items:
328 self.selected_item = (self.selected_item - 1) % len(items)
330 def _navigate_down(self):
331 """Navigate down in current section"""
332 current_section = self.sections[self.selected_section]
333 items = current_section.get('items', [])
334 if items:
335 self.selected_item = (self.selected_item + 1) % len(items)
337 def _navigate_left(self):
338 """Navigate to previous section"""
339 self.selected_section = (self.selected_section - 1) % len(self.sections)
340 self.selected_item = 0
342 def _navigate_right(self):
343 """Navigate to next section"""
344 self.selected_section = (self.selected_section + 1) % len(self.sections)
345 self.selected_item = 0
347 def _get_selected_action(self) -> str:
348 """Get the action for the currently selected item"""
349 try:
350 current_section = self.sections[self.selected_section]
351 items = current_section.get('items', [])
352 if items and 0 <= self.selected_item < len(items):
353 return items[self.selected_item].get('action', 'unknown')
354 except:
355 pass
356 return 'unknown'
358 def _get_action_by_number(self, number: int) -> str:
359 """Get action by number (1-9)"""
360 # Map numbers to sections/items in order
361 action_index = number - 1
362 total_actions = sum(len(section.get('items', [])) for section in self.sections)
364 if action_index < total_actions:
365 current_index = 0
366 for section in self.sections:
367 items = section.get('items', [])
368 if action_index < current_index + len(items):
369 item_index = action_index - current_index
370 return items[item_index].get('action', 'unknown')
371 current_index += len(items)
373 return 'unknown'
375 def _show_help(self, live_display):
376 """Show help overlay"""
377 help_content = """
378[bold cyan]OSIRIS COMMAND CENTER - HELP[/bold cyan]
380[dim]Navigation:[/dim]
381 [white]Arrow Keys[/white] - Move between sections and items
382 [white]Enter/Space[/white] - Select current item
383 [white]Numbers 1-9[/white] - Quick select actions
384 [white]Q[/white] - Quit/Go back
386[dim]Interface:[/dim]
387 [white]↑↓[/white] - Navigate items in current section
388 [white]←→[/white] - Switch between sections
389 [white]?[/white] - Show this help
391[dim]Sections:[/dim]
392 Each colored card represents a section
393 Green border = Currently selected section
394 Items shown with ▶ are selectable
396[dim]Press any key to continue...[/dim]
397 """
399 help_panel = Panel(
400 help_content,
401 title="Help - Command Center",
402 border_style="yellow",
403 padding=(1, 2)
404 )
406 live_display.update(help_panel)
407 live_display.refresh()
409 # Simple wait - just continue after showing help
410 import time
411 time.sleep(2)
413class CommandPalette:
414 """VS Code-style command palette"""
416 def __init__(self, commands: List[Dict[str, Any]]):
417 self.commands = commands
418 self.filtered_commands = commands.copy()
419 self.selected_index = 0
420 self.search_query = ""
422 def show(self) -> str:
423 """Show the command palette"""
424 import sys
426 # Enable raw input
427 old_settings = None
428 raw_mode = False
429 try:
430 import termios
431 import tty
432 old_settings = termios.tcgetattr(sys.stdin)
433 tty.setraw(sys.stdin.fileno())
434 raw_mode = True
435 except:
436 raw_mode = False
438 try:
439 with Live(console=console, refresh_per_second=30, auto_refresh=False) as live:
440 while True:
441 # Render palette
442 content = self._render_palette()
443 live.update(content)
444 live.refresh()
446 # Handle input
447 if raw_mode:
448 try:
449 import select
450 ready, _, _ = select.select([sys.stdin], [], [], 0.1)
451 if ready:
452 char = sys.stdin.read(1)
454 if char == '\r' or char == '\n': # Enter
455 if self.filtered_commands:
456 return self.filtered_commands[self.selected_index].get('action', 'unknown')
457 elif char == '\x7f' or char == '\b': # Backspace
458 self.search_query = self.search_query[:-1]
459 self._update_filter()
460 elif char == '\x1b': # Escape
461 seq = sys.stdin.read(2)
462 if seq == '[A': # Up
463 self.selected_index = max(0, self.selected_index - 1)
464 elif seq == '[B': # Down
465 self.selected_index = min(len(self.filtered_commands) - 1, self.selected_index + 1)
466 else:
467 return "cancel" # ESC
468 elif char in ['q', 'Q']:
469 return "cancel"
470 elif char.isprintable():
471 self.search_query += char
472 self._update_filter()
473 self.selected_index = 0
474 except:
475 pass
476 else:
477 # Fallback input
478 try:
479 char = input().strip()
480 if not char:
481 if self.filtered_commands:
482 return self.filtered_commands[self.selected_index].get('action', 'unknown')
483 elif char in ['q', 'quit']:
484 return "cancel"
485 except (EOFError, KeyboardInterrupt):
486 return "cancel"
488 except KeyboardInterrupt:
489 return "cancel"
490 finally:
491 if raw_mode and old_settings:
492 try:
493 import termios
494 termios.tcsetattr(sys.stdin, termios.TCSADRAIN, old_settings)
495 except:
496 pass
498 def _update_filter(self):
499 """Update filtered commands based on search query"""
500 if not self.search_query:
501 self.filtered_commands = self.commands.copy()
502 else:
503 query_lower = self.search_query.lower()
504 self.filtered_commands = [
505 cmd for cmd in self.commands
506 if query_lower in cmd.get('name', '').lower() or
507 query_lower in cmd.get('description', '').lower()
508 ]
510 def _render_palette(self) -> Panel:
511 """Render the command palette"""
512 terminal_width = console.size.width if hasattr(console, 'size') else 80
514 content = f"[bold cyan]Search Commands[/bold cyan]\n"
515 content += f"[dim]{'─' * min(terminal_width-4, 60)}[/dim]\n\n"
517 # Search input
518 search_display = self.search_query or "[dim]Type to search...[/dim]"
519 content += f"[white]❯ {search_display}[/white]\n\n"
521 # Show filtered results
522 if self.filtered_commands:
523 start_idx = max(0, self.selected_index - 5)
524 end_idx = min(len(self.filtered_commands), start_idx + 10)
526 for i in range(start_idx, end_idx):
527 cmd = self.filtered_commands[i]
528 name = cmd.get('name', 'Unknown')
529 desc = cmd.get('description', 'No description')
531 if i == self.selected_index:
532 content += f"[bold green]▶ {name}[/bold green]\n"
533 content += f" [dim]{desc}[/dim]\n"
534 else:
535 content += f"[dim] {name}[/dim]\n"
537 # Show count
538 total = len(self.filtered_commands)
539 content += f"\n[dim]{total} command{'s' if total != 1 else ''} found[/dim]"
540 else:
541 content += "[yellow]No commands found[/yellow]"
543 return Panel(
544 content,
545 title="Command Palette",
546 border_style="magenta",
547 padding=(1, 2)
548 )
552class MultiSelectMenu:
553 """Multi-select menu for choosing multiple options"""
555 def __init__(self, title: str, options: List[Dict[str, Any]]):
556 self.title = title
557 self.options = options
558 self.selected = set()
560 def display(self) -> List[str]:
561 """Display multi-select menu and return selected keys"""
562 while True:
563 # Clear screen for clean display
564 console.clear()
566 # Create menu table
567 menu_table = Table(box=HEAVY_HEAD, title=f"[bold magenta]{self.title}[/bold magenta]")
568 menu_table.add_column("✓", width=3, justify="center")
569 menu_table.add_column("Key", style="yellow", width=6, justify="center")
570 menu_table.add_column("Option", style="white")
571 menu_table.add_column("Description", style="dim")
573 for i, option in enumerate(self.options):
574 key = option.get('key', str(i + 1))
575 name = option.get('name', option.get('title', 'Unknown'))
576 desc = option.get('description', option.get('desc', ''))
578 checked = "✓" if key in self.selected else " "
579 style = "bold green" if key in self.selected else "white"
581 menu_table.add_row(
582 f"[{style}]{checked}[/{style}]",
583 f"[{style}]{key}[/{style}]",
584 f"[{style}]{name}[/{style}]",
585 desc
586 )
588 # Summary
589 selected_count = len(self.selected)
590 total_count = len(self.options)
592 summary = Panel(
593 f"[green]Selected: {selected_count}/{total_count}[/green]\n\n"
594 "[bold white]Space[/bold white] Toggle [bold white]A[/bold white] Select All [bold white]N[/bold white] Select None\n"
595 "[bold white]Enter[/bold white] Confirm [bold white]Esc[/bold white] Cancel",
596 title="Selection Summary",
597 border_style="green"
598 )
600 layout = Layout()
601 layout.split_column(
602 Layout(menu_table, name="menu"),
603 Layout(summary, name="summary", size=6)
604 )
606 console.print(layout)
608 # Get input
609 try:
610 choice = Prompt.ask("\n[bold cyan]Action[/bold cyan]").strip().lower()
612 if choice == "":
613 return list(self.selected)
614 elif choice == "a" or choice == "all":
615 self.selected = {opt.get('key', str(i+1)) for i, opt in enumerate(self.options)}
616 elif choice == "n" or choice == "none":
617 self.selected.clear()
618 elif choice in ["cancel", "c", "esc"]:
619 return []
620 else:
621 # Toggle specific option
622 for option in self.options:
623 if choice == str(option.get('key', '')).lower():
624 key = option['key']
625 if key in self.selected:
626 self.selected.remove(key)
627 else:
628 self.selected.add(key)
629 break
630 else:
631 console.print(f"[red]Invalid choice: {choice}[/red]")
632 Prompt.ask("[dim]Press Enter to continue...[/dim]")
634 except KeyboardInterrupt:
635 return []
636 except EOFError:
637 return []
639def convert_options_to_sections(title: str, options: List[Dict[str, Any]], icon: str = "📋") -> List[Dict[str, Any]]:
640 """Convert old options format to new sections format for ModernMenu"""
641 return [{
642 "title": title,
643 "icon": icon,
644 "items": [
645 {"name": opt.get("name", opt.get("title", "Unknown")),
646 "description": opt.get("description", opt.get("desc", "")),
647 "action": opt.get("key", "unknown")}
648 for opt in options
649 ]
650 }]
652class EnhancedOsirisCLI:
653 """Enhanced Rich CLI with interactive menus and better UX"""
655 def __init__(self):
656 self.running = True
657 self.current_mode = "menu" # chat, menu, tools, context, sessions - start in menu mode
658 self.agent_status = "Ready"
659 self.tool_history = [] # Keep track of tool executions
660 self.current_activity = "" # Current agent activity
662 def show_welcome(self):
663 """Show responsive welcome banner with dynamic sizing"""
664 # Get terminal dimensions for responsive layout
665 terminal_width = console.size.width if hasattr(console, 'size') else 80
667 # Responsive padding and sizing
668 padding_x = max(2, min(4, terminal_width // 40))
669 padding_y = max(1, min(2, terminal_width // 80))
671 # Responsive title
672 if terminal_width < 60:
673 title_text = "OSIRIS CLI v3.2.0"
674 subtitle_text = "AGI Terminal"
675 else:
676 title_text = "OSIRIS CLI v3.2.0 - ENHANCED EDITION"
677 subtitle_text = "Sovereign AGI Terminal Interface"
679 welcome_panel = Panel(
680 f"[bold cyan]{title_text}[/bold cyan]\n\n"
681 f"[bold white]{subtitle_text}[/bold white]\n\n"
682 f"[dim]Provider:[/dim] {settings.provider.upper()} | "
683 f"[dim]Model:[/dim] {settings.default_model}\n\n"
684 "[green]Interactive menus | Real-time navigation | Multi-select[/green]\n"
685 "[yellow]TAB: Main menu | /help: Commands | ↑↓: Navigate[/yellow]",
686 title="⚡ Interface Ready",
687 border_style="cyan",
688 box=DOUBLE,
689 padding=(padding_y, padding_x)
690 )
691 console.print(welcome_panel)
693 def show_dynamic_status(self):
694 """Show responsive status bar with agent activity"""
695 terminal_width = console.size.width if hasattr(console, 'size') else 80
697 status_parts = [
698 f"[bold cyan]Agent:[/bold cyan] {self.agent_status}",
699 ]
701 if self.current_activity:
702 # Truncate long activity messages for narrow terminals
703 activity = self.current_activity
704 if terminal_width < 80 and len(activity) > 20:
705 activity = activity[:17] + "..."
706 status_parts.append(f"[bold yellow]Activity:[/bold yellow] {activity}")
708 if self.tool_history:
709 recent_tool = self.tool_history[-1]
710 tool_info = f"{recent_tool['tool']} ({recent_tool['status']})"
711 if terminal_width < 80 and len(tool_info) > 25:
712 tool_info = tool_info[:22] + "..."
713 status_parts.append(f"[bold green]Last Tool:[/bold green] {tool_info}")
715 # Responsive status text layout
716 if terminal_width < 60:
717 # Stack status vertically for narrow terminals
718 status_text = "\n".join(status_parts)
719 else:
720 # Horizontal layout for wider terminals
721 status_text = " | ".join(status_parts)
723 status_panel = Panel(
724 status_text,
725 title="Live Status" if terminal_width >= 40 else "Status",
726 border_style="blue",
727 box=SIMPLE_HEAVY,
728 padding=(0, max(1, terminal_width // 80))
729 )
730 console.print(status_panel)
732 def show_main_menu(self):
733 """Show modern dashboard-style main menu"""
734 console.clear()
736 # Define menu sections for the dashboard
737 sections = [
738 {
739 "title": "Quick Actions",
740 "icon": "⚡",
741 "items": [
742 {"name": "Start Chat", "description": "Begin AI conversation", "action": "chat"},
743 {"name": "Command Palette", "description": "Quick command search", "action": "commands"},
744 {"name": "Help & Docs", "description": "Get help and docs", "action": "help"}
745 ]
746 },
747 {
748 "title": "AI Tools",
749 "icon": "🛠️",
750 "items": [
751 {"name": "Tool Manager", "description": "Browse available tools", "action": "tools"},
752 {"name": "Context Files", "description": "Manage context files", "action": "context"},
753 {"name": "Model Settings", "description": "Change AI model", "action": "model"}
754 ]
755 },
756 {
757 "title": "Session Hub",
758 "icon": "💾",
759 "items": [
760 {"name": "Load Session", "description": "Resume previous chat", "action": "load_session"},
761 {"name": "Save Session", "description": "Save current chat", "action": "save_session"},
762 {"name": "Session History", "description": "View all sessions", "action": "session_history"}
763 ]
764 },
765 {
766 "title": "System",
767 "icon": "⚙️",
768 "items": [
769 {"name": "System Status", "description": "View system info", "action": "status"},
770 {"name": "Preferences", "description": "Configure settings", "action": "settings"},
771 {"name": "Exit Osiris", "description": "Quit application", "action": "exit"}
772 ]
773 }
774 ]
776 # Create and show the modern menu
777 menu = ModernMenu("OSIRIS COMMAND CENTER", sections)
778 choice = menu.display()
780 # Handle the selected action
781 action_map = {
782 "chat": self.enter_chat_mode,
783 "commands": self.show_command_palette,
784 "help": self.show_help_menu,
785 "tools": self.show_tools_menu,
786 "context": self.show_context_menu,
787 "model": self.show_model_menu,
788 "load_session": lambda: console.print("[yellow]Session loading coming soon[/yellow]"),
789 "save_session": lambda: console.print("[yellow]Session saving coming soon[/yellow]"),
790 "session_history": self.show_sessions_menu,
791 "status": self.show_status,
792 "settings": self.show_settings_menu,
793 "exit": lambda: setattr(self, 'running', False)
794 }
796 action = action_map.get(choice)
797 if action:
798 if choice == "status":
799 action()
800 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
801 else:
802 action()
803 elif choice != "cancel":
804 console.print(f"[red]Unknown action: {choice}[/red]")
805 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
807 def show_help_menu(self):
808 """Show help and documentation menu"""
809 console.clear()
811 help_sections = [
812 {
813 "title": "Getting Started",
814 "icon": "🚀",
815 "items": [
816 {"name": "Quick Start", "description": "Basic usage guide", "action": "quick_start"},
817 {"name": "Navigation", "description": "How to use the interface", "action": "navigation_help"},
818 {"name": "First Chat", "description": "Start your first conversation", "action": "first_chat"}
819 ]
820 },
821 {
822 "title": "Advanced Features",
823 "icon": "🔧",
824 "items": [
825 {"name": "Tool Integration", "description": "Using AI tools", "action": "tool_guide"},
826 {"name": "Context Management", "description": "Working with files", "action": "context_guide"},
827 {"name": "Session Management", "description": "Saving and loading", "action": "session_guide"}
828 ]
829 },
830 {
831 "title": "Resources",
832 "icon": "📚",
833 "items": [
834 {"name": "Documentation", "description": "Full documentation", "action": "docs"},
835 {"name": "Examples", "description": "Usage examples", "action": "examples"},
836 {"name": "Back to Main", "description": "Return to main menu", "action": "back"}
837 ]
838 }
839 ]
841 menu = ModernMenu("Help & Documentation", help_sections)
842 choice = menu.display()
844 if choice == "back":
845 return
846 elif choice == "quick_start":
847 self.show_quick_start_guide()
848 elif choice == "navigation_help":
849 self.show_navigation_help()
850 elif choice == "first_chat":
851 self.enter_chat_mode()
852 elif choice == "tool_guide":
853 self.show_tool_guide()
854 elif choice == "context_guide":
855 self.show_context_guide()
856 elif choice == "session_guide":
857 self.show_session_guide()
858 elif choice == "docs":
859 self.show_documentation()
860 elif choice == "examples":
861 self.show_examples()
863 def show_model_menu(self):
864 """Show AI model selection menu"""
865 console.clear()
867 model_sections = [
868 {
869 "title": "OpenAI Models",
870 "icon": "🤖",
871 "items": [
872 {"name": "GPT-4", "description": "Most capable model", "action": "set_gpt4"},
873 {"name": "GPT-3.5 Turbo", "description": "Fast and efficient", "action": "set_gpt35"},
874 {"name": "GPT-4 Turbo", "description": "Latest GPT-4 variant", "action": "set_gpt4t"}
875 ]
876 },
877 {
878 "title": "Other Providers",
879 "icon": "🌐",
880 "items": [
881 {"name": "Claude 3.5 Sonnet", "description": "Anthropic's best", "action": "set_claude"},
882 {"name": "Gemini Pro", "description": "Google's model", "action": "set_gemini"},
883 {"name": "Back to Main", "description": "Return to main menu", "action": "back"}
884 ]
885 }
886 ]
888 menu = ModernMenu("Select AI Model", model_sections)
889 choice = menu.display()
891 model_map = {
892 "set_gpt4": ("openai", "gpt-4"),
893 "set_gpt35": ("openai", "gpt-3.5-turbo"),
894 "set_gpt4t": ("openai", "gpt-4-turbo"),
895 "set_claude": ("anthropic", "claude-3-5-sonnet-20241022"),
896 "set_gemini": ("google", "gemini-pro")
897 }
899 if choice in model_map:
900 provider, model = model_map[choice]
901 settings.provider = provider
902 settings.default_model = model
903 settings.save()
904 console.print(f"[green]Model changed to: {model} ({provider})[/green]")
905 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
906 elif choice == "back":
907 return
909 # Placeholder methods for new features
910 def show_quick_start_guide(self):
911 """Show quick start guide"""
912 console.clear()
913 console.print("[bold cyan]Quick Start Guide[/bold cyan]")
914 console.print("\n[dim]Welcome to Osiris CLI![/dim]")
915 console.print("\n1. Use arrow keys to navigate the dashboard")
916 console.print("2. Press Enter to select an option")
917 console.print("3. Start with 'Start Chat' to begin AI conversation")
918 console.print("4. Use 'Command Palette' for quick actions")
919 console.print("\n[dim]Press ? anytime for help[/dim]")
920 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
922 def show_navigation_help(self):
923 """Show navigation help"""
924 console.clear()
925 console.print("[bold cyan]Navigation Guide[/bold cyan]")
926 console.print("\n[white]Arrow Keys:[/white] Navigate between sections and items")
927 console.print("[white]Enter/Space:[/white] Select current item")
928 console.print("[white]Numbers 1-9:[/white] Quick select actions")
929 console.print("[white]Q:[/white] Quit or go back")
930 console.print("[white]?:[/white] Show help")
931 console.print("\n[dim]Green borders indicate selected sections[/dim]")
932 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
934 def show_tool_guide(self):
935 """Show tool usage guide"""
936 console.clear()
937 console.print("[bold cyan]Tool Integration Guide[/bold cyan]")
938 console.print("\n[dim]Osiris can use various tools during conversations:[/dim]")
939 console.print("\n• [white]run_shell_command:[/white] Execute system commands")
940 console.print("• [white]read_file:[/white] Analyze file contents")
941 console.print("• [white]write_file:[/white] Create or modify files")
942 console.print("• [white]web_search:[/white] Search the internet")
943 console.print("\n[dim]Tools are automatically used when relevant to your queries[/dim]")
944 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
946 def show_context_guide(self):
947 """Show context management guide"""
948 console.clear()
949 console.print("[bold cyan]Context Management Guide[/bold cyan]")
950 console.print("\n[dim]Context files help AI understand your project:[/dim]")
951 console.print("\n• Add relevant files for better responses")
952 console.print("• Remove files to reduce context size")
953 console.print("• Context persists across conversations")
954 console.print("\n[dim]Use the Context section in the main menu[/dim]")
955 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
957 def show_session_guide(self):
958 """Show session management guide"""
959 console.clear()
960 console.print("[bold cyan]Session Management Guide[/bold cyan]")
961 console.print("\n[dim]Save and resume conversations:[/dim]")
962 console.print("\n• Sessions save your chat history")
963 console.print("• Load previous conversations")
964 console.print("• View session statistics")
965 console.print("\n[dim]Use the Session Hub in the main menu[/dim]")
966 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
968 def show_documentation(self):
969 """Show full documentation"""
970 console.clear()
971 console.print("[bold cyan]Osiris CLI Documentation[/bold cyan]")
972 console.print("\n[dim]Full documentation available at:[/dim]")
973 console.print("[blue]https://github.com/your-repo/osiris-cli[/blue]")
974 console.print("\n[dim]Features:[/dim]")
975 console.print("• AI-powered conversations")
976 console.print("• Tool integration")
977 console.print("• Context management")
978 console.print("• Session persistence")
979 console.print("• Multiple AI providers")
980 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
982 def show_examples(self):
983 """Show usage examples"""
984 console.clear()
985 console.print("[bold cyan]Usage Examples[/bold cyan]")
986 console.print("\n[dim]Try these example queries:[/dim]")
987 console.print("\n[white]•[/white] 'List files in the current directory'")
988 console.print("[white]•[/white] 'Create a Python script that prints hello world'")
989 console.print("[white]•[/white] 'Analyze the code in main.py'")
990 console.print("[white]•[/white] 'What is the weather like today?'")
991 console.print("\n[dim]Osiris will use appropriate tools automatically[/dim]")
992 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
996 def enter_chat_mode(self):
997 """Enter responsive interactive chat mode"""
998 console.clear()
1000 # Get terminal dimensions for responsive layout
1001 terminal_width = console.size.width if hasattr(console, 'size') else 80
1002 terminal_height = console.size.height if hasattr(console, 'size') else 24
1004 self.show_welcome()
1005 self.show_dynamic_status()
1007 # Responsive tool history display
1008 if self.tool_history:
1009 history_title = "[bold blue]Recent Tool Activity:[/bold blue]"
1010 console.print(f"\n{history_title}")
1012 # Show appropriate number based on terminal height
1013 max_tools = max(2, min(5, terminal_height // 4))
1014 for tool in self.tool_history[-max_tools:]:
1015 status_icon = "[green]OK[/green]" if tool['status'] == 'success' else "[red]ERROR[/red]" if tool['status'] == 'error' else "[yellow]PENDING[/yellow]"
1017 # Truncate for narrow terminals
1018 tool_info = f"{tool['tool']}: {tool['description'][:40]}..." if len(tool['description']) > 40 else f"{tool['tool']}: {tool['description']}"
1019 if terminal_width < 60:
1020 tool_info = tool_info[:30] + "..." if len(tool_info) > 30 else tool_info
1022 console.print(f"{status_icon} {tool_info}")
1023 console.print()
1025 # Responsive chat panel
1026 if terminal_width < 50:
1027 chat_content = (
1028 "[bold green]Chat Active[/bold green]\n\n"
1029 "[white]Commands:[/white]\n"
1030 "[cyan]/m[/cyan] - menu\n"
1031 "[cyan]/c[/cyan] - clear\n"
1032 "[cyan]/h[/cyan] - help"
1033 )
1034 else:
1035 chat_content = (
1036 "[bold green]Chat Mode Active[/bold green]\n\n"
1037 "[white]Type your message or use commands:[/white]\n"
1038 "[cyan]/menu[/cyan] - Return to main menu\n"
1039 "[cyan]/clear[/cyan] - Clear conversation\n"
1040 "[cyan]/help[/cyan] - Show commands"
1041 )
1043 chat_panel = Panel(
1044 chat_content,
1045 title="Chat Interface" if terminal_width >= 40 else "Chat",
1046 border_style="green",
1047 box=DOUBLE,
1048 padding=(max(1, terminal_width // 80), max(1, terminal_width // 40))
1049 )
1050 console.print(chat_panel)
1052 self.current_mode = "chat"
1054 def show_tools_menu(self):
1055 """Show responsive tools management menu"""
1056 console.clear()
1058 # Get terminal dimensions
1059 terminal_width = console.size.width if hasattr(console, 'size') else 80
1061 # Responsive tool list
1062 if terminal_width < 50:
1063 # Compact tool list for narrow terminals
1064 tool_options = [
1065 {"key": "1", "name": "Shell Cmd", "description": "Run commands"},
1066 {"key": "2", "name": "Read File", "description": "View files"},
1067 {"key": "3", "name": "Write File", "description": "Edit files"},
1068 {"key": "4", "name": "List Dir", "description": "Browse dirs"},
1069 {"key": "5", "name": "Web Search", "description": "Search web"},
1070 {"key": "6", "name": "Code Search", "description": "Find code"},
1071 {"key": "0", "name": "Back", "description": "Main menu"},
1072 ]
1073 else:
1074 # Full tool list
1075 tool_options = [
1076 {"key": "1", "name": "run_shell_command", "description": "Execute shell commands on system"},
1077 {"key": "2", "name": "read_file", "description": "Read and analyze file contents"},
1078 {"key": "3", "name": "write_file", "description": "Create or modify files"},
1079 {"key": "4", "name": "list_directory", "description": "List directory contents"},
1080 {"key": "5", "name": "web_search", "description": "Search the web for information"},
1081 {"key": "6", "name": "search_codebase", "description": "Search through code repositories"},
1082 {"key": "0", "name": "Back", "description": "Return to main menu"},
1083 ]
1085 # Convert to sections format for modern menu
1086 tool_sections = [{
1087 "title": "Tool Manager",
1088 "icon": "🛠️",
1089 "items": [{"name": opt["name"], "description": opt["description"], "action": opt["key"]}
1090 for opt in tool_options if opt["key"] != "0"]
1091 }]
1092 tool_sections.append({
1093 "title": "Actions",
1094 "icon": "⚡",
1095 "items": [{"name": "Back to Main", "description": "Return to main menu", "action": "0"}]
1096 })
1097 menu = ModernMenu("Tool Manager", tool_sections)
1098 choice = menu.display()
1100 if choice == "0":
1101 return
1102 elif choice == "cancel":
1103 return
1105 # Show tool info with responsive display
1106 tool_names = ["run_shell_command", "read_file", "write_file", "list_directory", "web_search", "search_codebase"]
1107 try:
1108 tool_index = int(choice) - 1
1109 if 0 <= tool_index < len(tool_names):
1110 selected_tool = tool_names[tool_index]
1111 self.show_tool_details(selected_tool)
1112 except (ValueError, IndexError):
1113 console.print(f"[red]Invalid tool selection: {choice}[/red]")
1114 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
1116 def show_tool_details(self, tool_name: str):
1117 """Show responsive detailed information about a specific tool"""
1118 terminal_width = console.size.width if hasattr(console, 'size') else 80
1120 # Try to get tool info
1121 tool_info = {}
1122 try:
1123 if hasattr(tools, 'tools') and tools.tools:
1124 tool_info = tools.tools.get(tool_name, {})
1125 except:
1126 pass
1128 # Responsive content
1129 description = tool_info.get('description', 'Execute system commands, run scripts, install packages, etc.')
1130 parameters = tool_info.get('parameters', {})
1132 if terminal_width < 50:
1133 # Compact display for narrow terminals
1134 content = (
1135 f"[bold cyan]{tool_name}[/bold cyan]\n\n"
1136 f"[white]{description[:100]}{'...' if len(description) > 100 else ''}[/white]\n\n"
1137 "[dim]Available in AI conversations[/dim]"
1138 )
1139 title = f"Tool: {tool_name[:15]}..."
1140 else:
1141 # Full display
1142 param_info = "None"
1143 if parameters:
1144 param_list = []
1145 if 'properties' in parameters:
1146 for param_name, param_info in parameters['properties'].items():
1147 param_type = param_info.get('type', 'string')
1148 param_desc = param_info.get('description', '')[:30]
1149 param_list.append(f"• {param_name} ({param_type}): {param_desc}")
1150 param_info = "\n".join(param_list[:3]) # Show max 3 params
1152 content = (
1153 f"[bold cyan]Tool: {tool_name}[/bold cyan]\n\n"
1154 f"[bold white]Description:[/bold white]\n{description}\n\n"
1155 f"[bold white]Parameters:[/bold white]\n{param_info}\n\n"
1156 "[dim]This tool integrates seamlessly with AI conversations[/dim]"
1157 )
1158 title = f"Tool Details: {tool_name}"
1160 details_panel = Panel(
1161 content,
1162 title=title,
1163 border_style="blue",
1164 box=DOUBLE,
1165 padding=(max(1, terminal_width // 80), max(2, terminal_width // 40))
1166 )
1168 console.print(details_panel)
1169 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
1171 def show_context_menu(self):
1172 """Show context management menu"""
1173 console.clear()
1175 if not context.loaded_files:
1176 context_options = [
1177 {"key": "1", "name": "Add Files", "description": "Add files to context"},
1178 {"key": "2", "name": "Browse Directory", "description": "Browse and select files"},
1179 {"key": "0", "name": "Back", "description": "Return to main menu"},
1180 ]
1181 else:
1182 # Show current context files
1183 context_table = Table(title="📄 Current Context Files")
1184 context_table.add_column("File", style="cyan")
1185 context_table.add_column("Size", style="white", justify="right")
1186 context_table.add_column("Actions", style="yellow")
1188 for file_path, content in context.loaded_files.items():
1189 size = len(str(content))
1190 context_table.add_row(file_path, f"{size:,} chars", "Remove")
1192 console.print(context_table)
1193 console.print()
1195 context_options = [
1196 {"key": "1", "name": "Add More Files", "description": "Add additional files"},
1197 {"key": "2", "name": "Remove Files", "description": "Remove files from context"},
1198 {"key": "3", "name": "Clear All", "description": "Clear entire context"},
1199 {"key": "0", "name": "Back", "description": "Return to main menu"},
1200 ]
1202 context_sections = convert_options_to_sections("Context Management", context_options, "📁")
1203 menu = ModernMenu("Context Manager", context_sections)
1204 choice = menu.display()
1206 if choice == "1":
1207 self.add_files_to_context()
1208 elif choice == "2" and context.loaded_files:
1209 self.remove_files_from_context()
1210 elif choice == "3" and context.loaded_files:
1211 if Confirm.ask("Clear all context files?"):
1212 context.clear()
1213 console.print("[green]Context cleared[/green]")
1214 elif choice == "0":
1215 return
1217 def add_files_to_context(self):
1218 """Add files to context with multi-select"""
1219 try:
1220 # Get current directory files
1221 current_dir = Path.cwd()
1222 files = []
1224 for item in current_dir.iterdir():
1225 if item.is_file() and not item.name.startswith('.'):
1226 files.append({
1227 "key": item.name,
1228 "name": item.name,
1229 "description": f"{item.stat().st_size:,} bytes"
1230 })
1232 if not files:
1233 console.print("[yellow]📁 No files found in current directory[/yellow]")
1234 return
1236 # Multi-select menu
1237 multi_menu = MultiSelectMenu("📁 Select Files to Add", files)
1238 selected_files = multi_menu.display()
1240 if selected_files:
1241 added_count = 0
1242 for filename in selected_files:
1243 if context.add_file(filename):
1244 added_count += 1
1246 console.print(f"[green]Added {added_count} files to context[/green]")
1247 else:
1248 console.print("[yellow]⚠️ No files selected[/yellow]")
1250 except Exception as e:
1251 console.print(f"[red]Error adding files: {e}[/red]")
1253 def remove_files_from_context(self):
1254 """Remove files from context with multi-select"""
1255 if not context.loaded_files:
1256 return
1258 # Convert loaded files to menu options
1259 file_options = []
1260 for file_path in context.loaded_files.keys():
1261 file_options.append({
1262 "key": file_path,
1263 "name": Path(file_path).name,
1264 "description": file_path
1265 })
1267 multi_menu = MultiSelectMenu("🗑️ Select Files to Remove", file_options)
1268 selected_files = multi_menu.display()
1270 if selected_files:
1271 removed_count = 0
1272 for file_path in selected_files:
1273 if context.remove_file(file_path):
1274 removed_count += 1
1276 console.print(f"[green]Removed {removed_count} files from context[/green]")
1277 else:
1278 console.print("[yellow]⚠️ No files selected[/yellow]")
1280 def show_sessions_menu(self):
1281 """Show session management menu"""
1282 console.clear()
1284 sessions_list = session.list_session_meta()
1285 if not sessions_list:
1286 console.print("[yellow]No saved sessions found[/yellow]")
1287 console.print("[dim]Start a conversation and use /save to create sessions[/dim]")
1288 return
1290 # Show sessions table
1291 sessions_table = Table(title="Available Sessions")
1292 sessions_table.add_column("ID", style="cyan", width=12)
1293 sessions_table.add_column("Messages", style="white", justify="right")
1294 sessions_table.add_column("Created", style="dim")
1295 sessions_table.add_column("Preview", style="white")
1297 for sess_data in sessions_list:
1298 sess_id = sess_data.get('id', 'unknown')
1299 messages = sess_data.get('messages', [])
1300 created = sess_data.get('created', 'Unknown')
1301 preview = ""
1302 if messages:
1303 last_msg = messages[-1] if messages else {}
1304 preview = last_msg.get('content', '')[:50] + "..."
1306 sessions_table.add_row(sess_id[:12], str(len(messages)), created, preview)
1308 console.print(sessions_table)
1309 console.print()
1311 # Session actions menu
1312 session_options = [
1313 {"key": "1", "name": "Load Session", "description": "Load a saved session"},
1314 {"key": "2", "name": "Delete Session", "description": "Delete a session"},
1315 {"key": "3", "name": "Session Stats", "description": "Show session statistics"},
1316 {"key": "0", "name": "Back", "description": "Return to main menu"},
1317 ]
1319 session_sections = convert_options_to_sections("Session Management", session_options, "💾")
1320 menu = ModernMenu("Session Hub", session_sections)
1321 choice = menu.display()
1323 if choice == "1":
1324 self.load_session_interactive(sessions_list)
1325 elif choice == "2":
1326 self.delete_session_interactive(sessions_list)
1327 elif choice == "3":
1328 self.show_session_stats(sessions_list)
1330 def load_session_interactive(self, sessions_list):
1331 """Load a session with menu selection"""
1332 if not sessions_list:
1333 return
1335 session_options = []
1336 for sess_id in sessions_list.keys():
1337 session_options.append({
1338 "key": sess_id,
1339 "name": sess_id[:20],
1340 "description": f"Load session {sess_id}"
1341 })
1343 load_sections = convert_options_to_sections("Load Session", session_options, "📂")
1344 menu = ModernMenu("Load Session", load_sections)
1345 choice = menu.display()
1347 if choice in sessions_list:
1348 if session.load_session(choice):
1349 console.print(f"[green]Loaded session: {choice}[/green]")
1350 else:
1351 console.print(f"[red]Failed to load session: {choice}[/red]")
1352 elif choice != "cancel":
1353 console.print(f"[red]Invalid session: {choice}[/red]")
1355 def delete_session_interactive(self, sessions_list):
1356 """Delete a session with menu selection"""
1357 if not sessions_list:
1358 return
1360 session_options = []
1361 for sess_id in sessions_list.keys():
1362 session_options.append({
1363 "key": sess_id,
1364 "name": sess_id[:20],
1365 "description": f"Delete session {sess_id}"
1366 })
1368 delete_sections = convert_options_to_sections("Delete Session", session_options, "🗑️")
1369 menu = ModernMenu("Delete Session", delete_sections)
1370 choice = menu.display()
1372 if choice in sessions_list:
1373 if Confirm.ask(f"Delete session '{choice}'?"):
1374 # Note: session.delete_session() might not exist, this is a placeholder
1375 console.print(f"[green]Deleted session: {choice}[/green]")
1376 else:
1377 console.print("[yellow]Deletion cancelled[/yellow]")
1378 elif choice != "cancel":
1379 console.print(f"[red]Invalid session: {choice}[/red]")
1381 def show_session_stats(self, sessions_list):
1382 """Show session statistics"""
1383 total_sessions = len(sessions_list)
1384 total_messages = sum(len(sess.get('messages', [])) for sess in sessions_list.values())
1386 stats_panel = Panel(
1387 f"[bold white]Total Sessions:[/bold white] {total_sessions}\n"
1388 f"[bold white]Total Messages:[/bold white] {total_messages}\n"
1389 f"[bold white]Average per Session:[/bold white] {total_messages/total_sessions:.1f} messages",
1390 title="Session Statistics",
1391 border_style="blue"
1392 )
1394 console.print(stats_panel)
1396 def show_settings_menu(self):
1397 """Show settings management menu"""
1398 console.clear()
1400 settings_options = [
1401 {"key": "1", "name": "Change Model", "description": "Switch AI model"},
1402 {"key": "2", "name": "Change Provider", "description": "Switch AI provider"},
1403 {"key": "3", "name": "Change Theme", "description": "Change UI theme"},
1404 {"key": "4", "name": "Advanced Settings", "description": "Configure advanced options"},
1405 {"key": "0", "name": "Back", "description": "Return to settings menu"},
1406 ]
1408 settings_sections = convert_options_to_sections("Settings", settings_options, "⚙️")
1409 menu = ModernMenu("Configuration", settings_sections)
1410 choice = menu.display()
1412 if choice == "1":
1413 self.change_model()
1414 elif choice == "2":
1415 self.change_provider()
1416 elif choice == "3":
1417 self.change_theme()
1418 elif choice == "4":
1419 self.show_advanced_settings()
1421 def change_model(self):
1422 """Change AI model"""
1423 models = ["gpt-4", "gpt-3.5-turbo", "claude-3-5-sonnet-20241022", "gemini-pro"]
1425 model_options = []
1426 for i, model in enumerate(models):
1427 model_options.append({
1428 "key": str(i + 1),
1429 "name": model,
1430 "description": f"Select {model} model"
1431 })
1433 model_sections = convert_options_to_sections("AI Models", model_options, "🤖")
1434 menu = ModernMenu("Choose Model", model_sections)
1435 choice = menu.display()
1437 try:
1438 model_index = int(choice) - 1
1439 if 0 <= model_index < len(models):
1440 settings.default_model = models[model_index]
1441 settings.save()
1442 console.print(f"[green]Model changed to: {models[model_index]}[/green]")
1443 else:
1444 console.print(f"[red]Invalid model selection: {choice}[/red]")
1445 except (ValueError, IndexError):
1446 console.print(f"[red]Invalid choice: {choice}[/red]")
1448 def change_provider(self):
1449 """Change AI provider"""
1450 providers = ["openai", "anthropic", "google", "groq"]
1452 provider_options = []
1453 for i, provider in enumerate(providers):
1454 provider_options.append({
1455 "key": str(i + 1),
1456 "name": provider.upper(),
1457 "description": f"Switch to {provider} provider"
1458 })
1460 provider_sections = convert_options_to_sections("AI Providers", provider_options, "🔗")
1461 menu = ModernMenu("Choose Provider", provider_sections)
1462 choice = menu.display()
1464 try:
1465 provider_index = int(choice) - 1
1466 if 0 <= provider_index < len(providers):
1467 settings.provider = providers[provider_index]
1468 settings.save()
1469 console.print(f"[green]Provider changed to: {providers[provider_index].upper()}[/green]")
1470 else:
1471 console.print(f"[red]Invalid provider selection: {choice}[/red]")
1472 except (ValueError, IndexError):
1473 console.print(f"[red]Invalid choice: {choice}[/red]")
1475 def change_theme(self):
1476 """Change UI theme"""
1477 themes = ["default", "cyberpunk", "minimal", "ocean"]
1479 theme_options = []
1480 for i, theme in enumerate(themes):
1481 theme_options.append({
1482 "key": str(i + 1),
1483 "name": theme.title(),
1484 "description": f"Switch to {theme} theme"
1485 })
1487 theme_sections = convert_options_to_sections("Themes", theme_options, "🎨")
1488 menu = ModernMenu("Choose Theme", theme_sections)
1489 choice = menu.display()
1491 try:
1492 theme_index = int(choice) - 1
1493 if 0 <= theme_index < len(themes):
1494 settings.theme = themes[theme_index]
1495 settings.save()
1496 console.print(f"[green]Theme changed to: {themes[theme_index].title()}[/green]")
1497 else:
1498 console.print(f"[red]Invalid theme selection: {choice}[/red]")
1499 except (ValueError, IndexError):
1500 console.print(f"[red]Invalid choice: {choice}[/red]")
1502 def show_advanced_settings(self):
1503 """Show advanced settings menu"""
1504 advanced_options = [
1505 {"key": "1", "name": "🌡️ Temperature", "description": f"Current: {settings.temperature}"},
1506 {"key": "2", "name": "Reasoning Mode", "description": f"Current: {'Enabled' if settings.reasoning_mode else 'Disabled'}"},
1507 {"key": "3", "name": "⏱️ Timeout", "description": f"Current: {settings.timeout}s"},
1508 {"key": "4", "name": "🔧 YOLO Mode", "description": f"Current: {'Enabled' if settings.yolo_mode else 'Disabled'}"},
1509 {"key": "0", "name": "⬅️ Back", "description": "Return to settings menu"},
1510 ]
1512 advanced_sections = convert_options_to_sections("Advanced", advanced_options, "🔧")
1513 menu = ModernMenu("Advanced Settings", advanced_sections)
1514 choice = menu.display()
1516 if choice == "1":
1517 temp = Prompt.ask("Enter temperature (0.0-2.0)", default=str(settings.temperature))
1518 try:
1519 settings.temperature = float(temp)
1520 settings.save()
1521 console.print(f"[green]Temperature set to: {settings.temperature}[/green]")
1522 except ValueError:
1523 console.print("[red]Invalid temperature value[/red]")
1524 elif choice == "2":
1525 settings.reasoning_mode = not settings.reasoning_mode
1526 settings.save()
1527 status = "Enabled" if settings.reasoning_mode else "Disabled"
1528 console.print(f"[green]Reasoning mode: {status}[/green]")
1529 elif choice == "3":
1530 timeout = Prompt.ask("Enter timeout (seconds)", default=str(settings.timeout))
1531 try:
1532 settings.timeout = int(timeout)
1533 settings.save()
1534 console.print(f"[green]Timeout set to: {settings.timeout}s[/green]")
1535 except ValueError:
1536 console.print("[red]Invalid timeout value[/red]")
1537 elif choice == "4":
1538 settings.yolo_mode = not settings.yolo_mode
1539 settings.save()
1540 status = "Enabled" if settings.yolo_mode else "Disabled"
1541 console.print(f"[green]YOLO mode: {status}[/green]")
1543 def show_command_palette(self):
1544 """Show quick command palette"""
1545 commands = [
1546 {"key": "/help", "name": "Help", "description": "Show available commands"},
1547 {"key": "/clear", "name": "Clear", "description": "Clear conversation"},
1548 {"key": "/save", "name": "Save", "description": "Save current session"},
1549 {"key": "/load", "name": "Load", "description": "Load previous session"},
1550 {"key": "/status", "name": "Status", "description": "Show system status"},
1551 {"key": "/tools", "name": "Tools", "description": "List available tools"},
1552 {"key": "/context", "name": "Context", "description": "Show context files"},
1553 {"key": "/menu", "name": "Menu", "description": "Return to main menu"},
1554 {"key": "/exit", "name": "Exit", "description": "Quit application"},
1555 ]
1557 palette_table = Table(title="Command Palette")
1558 palette_table.add_column("Command", style="cyan")
1559 palette_table.add_column("Description", style="white")
1561 for cmd in commands:
1562 palette_table.add_row(cmd["key"], cmd["description"])
1564 console.print(palette_table)
1566 def show_status(self):
1567 """Show system status (enhanced version)"""
1568 console.clear()
1570 stats = session.get_session_stats()
1572 # Create multi-panel layout
1573 layout = Layout()
1575 # System info panel
1576 system_panel = Panel(
1577 f"[bold white]Version:[/bold white] 3.2.0 Enhanced\n"
1578 f"[bold white]Provider:[/bold white] {settings.provider.upper()}\n"
1579 f"[bold white]Model:[/bold white] {settings.default_model}\n"
1580 f"[bold white]Theme:[/bold white] {settings.theme.title()}\n"
1581 f"[bold white]YOLO Mode:[/bold white] {'Enabled' if settings.yolo_mode else 'Disabled'}",
1582 title="System Configuration",
1583 border_style="cyan"
1584 )
1586 # Session stats panel
1587 session_panel = Panel(
1588 f"[bold white]Total Sessions:[/bold white] {stats.get('total_sessions', 0)}\n"
1589 f"[bold white]Total Messages:[/bold white] {stats.get('total_messages', 0)}\n"
1590 f"[bold white]Total Tokens:[/bold white] {stats.get('total_tokens', 0):,}\n"
1591 f"[bold white]Context Files:[/bold white] {len(context.loaded_files)}\n"
1592 f"[bold white]Active Tools:[/bold white] {len(tools.tools)}",
1593 title="Session Statistics",
1594 border_style="green"
1595 )
1597 # Performance panel
1598 perf_panel = Panel(
1599 "[bold green]System Status: Optimal[/bold green]\n\n"
1600 "[white]• Configuration: Loaded[/white]\n"
1601 "[white]• AI Connection: Active[/white]\n"
1602 "[white]• Tool Integration: Ready[/white]\n"
1603 "[white]• Session Storage: Available[/white]",
1604 title="Performance Status",
1605 border_style="magenta"
1606 )
1608 layout.split_column(
1609 Layout(system_panel, name="system", size=8),
1610 Layout(session_panel, name="stats", size=8),
1611 Layout(perf_panel, name="perf", size=8)
1612 )
1614 console.print(layout)
1616 def run(self):
1617 """Main CLI loop with enhanced navigation"""
1618 console.clear()
1619 self.show_welcome()
1620 self.show_dynamic_status()
1622 while self.running:
1623 try:
1624 if self.current_mode == "menu":
1625 # Main menu mode - this handles its own input
1626 choice = self._show_main_menu_with_input()
1628 # Handle menu choice
1629 if choice == "1": # Chat
1630 self.enter_chat_mode()
1631 elif choice == "2": # Tools
1632 self.show_tools_menu()
1633 elif choice == "3": # Context
1634 self.show_context_menu()
1635 elif choice == "4": # Sessions
1636 self.show_sessions_menu()
1637 elif choice == "5": # Settings
1638 self.show_settings_menu()
1639 elif choice == "6": # Status
1640 self.show_status()
1641 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
1642 elif choice == "7": # Commands
1643 self.show_command_palette()
1644 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
1645 elif choice == "0": # Exit
1646 self.running = False
1647 elif choice == "cancel":
1648 continue # Stay in menu
1649 else:
1650 console.print(f"[red]Unknown menu choice: {choice}[/red]")
1651 Prompt.ask("\n[dim]Press Enter to continue...[/dim]")
1653 elif self.current_mode == "chat":
1654 # Chat mode - get user input
1655 console.print() # Add spacing
1656 user_input = Prompt.ask("[bold cyan]You[/bold cyan]").strip()
1658 if not user_input:
1659 continue
1661 if user_input.lower() in ["/menu", "/m", "tab"]:
1662 self.current_mode = "menu"
1663 continue
1664 elif user_input.startswith("/"):
1665 self.handle_command(user_input)
1666 else:
1667 # Process as AI message
1668 console.print("[dim]Processing...[/dim]")
1669 asyncio.run(self.process_message(user_input))
1671 except KeyboardInterrupt:
1672 console.print("\n[yellow]Goodbye![/yellow]")
1673 break
1674 except EOFError:
1675 break
1676 except Exception as e:
1677 console.print(f"[red]Error: {e}[/red]")
1678 import traceback
1679 traceback.print_exc()
1681 def _show_main_menu_with_input(self):
1682 """Show main menu and handle input in one go"""
1683 options = [
1684 {"key": "1", "name": "Chat", "description": "Start AI conversation"},
1685 {"key": "2", "name": "Tools", "description": "Browse and manage tools"},
1686 {"key": "3", "name": "Context", "description": "Manage context files"},
1687 {"key": "4", "name": "Sessions", "description": "Load/save conversations"},
1688 {"key": "5", "name": "Settings", "description": "Configure preferences"},
1689 {"key": "6", "name": "Status", "description": "System information"},
1690 {"key": "7", "name": "Commands", "description": "Quick command palette"},
1691 {"key": "0", "name": "Exit", "description": "Quit application"},
1692 ]
1694 main_sections = convert_options_to_sections("Main Menu", options, "🏠")
1695 menu = ModernMenu("Navigation", main_sections)
1696 return menu.display()
1698 def _get_input_with_tab(self):
1699 """Get user input with TAB support for menu switching"""
1700 # Simple prompt-based input with TAB detection
1701 console.print("[bold cyan]You[/bold cyan] ", end="")
1703 try:
1704 # Use Prompt.ask which handles basic input well
1705 user_input = Prompt.ask("", show_default=False, default="").strip()
1707 # Check if TAB was pressed (Prompt.ask might not detect it well)
1708 # For now, just use regular input and check for special commands
1709 if user_input.lower() in ['tab', '/menu', '/m']:
1710 return "TAB"
1711 return user_input
1713 except (EOFError, KeyboardInterrupt):
1714 return ""
1716 async def process_message(self, message: str):
1717 """Process a user message through the AI with enhanced UI"""
1718 try:
1719 # Update status
1720 self.agent_status = "Thinking"
1721 self.current_activity = "Processing your message..."
1723 # Add to session
1724 session.add_message("user", message)
1726 # Prepare system prompt
1727 sys_prompt = settings.system_prompt
1728 if settings.reasoning_mode:
1729 sys_prompt += "\n[REASONING ENABLED]"
1730 if context.loaded_files:
1731 sys_prompt += context.get_system_prompt_addition()
1733 if not session.messages:
1734 session.messages.append({"role": "system", "content": sys_prompt})
1736 # Get tool definitions - fix the tools access
1737 tool_registry = {}
1738 try:
1739 tool_registry = tools.tools if hasattr(tools, 'tools') else {}
1740 t_defs = [{"type": "function", "function": t["function"]} for t in tool_registry.values()]
1741 except:
1742 t_defs = [] # Fallback if tools don't work
1744 # Process with AI
1745 console.print()
1747 with Live(console=console, refresh_per_second=4) as live:
1748 accumulated_response = ""
1749 tool_calls_made = []
1751 for attempt in range(5):
1752 if not self.running:
1753 break
1755 self.current_activity = f"AI processing (attempt {attempt + 1}/5)..."
1756 live.update(Panel(
1757 f"[yellow]{self.current_activity}[/yellow]\n\n"
1758 "[dim]Waiting for AI response...[/dim]",
1759 title="Agent Status",
1760 border_style="yellow"
1761 ))
1763 response = await async_client.chat(session.messages, tools=t_defs)
1765 if isinstance(response, str):
1766 # Error response
1767 self.agent_status = "Error"
1768 self.current_activity = "Request failed"
1769 error_panel = Panel(
1770 f"[red]Error: {response}[/red]",
1771 title="AI Error",
1772 border_style="red"
1773 )
1774 live.update(error_panel)
1775 time.sleep(2) # Show error briefly
1776 return
1778 msg = response.choices[0].message
1779 content = msg.content or ""
1780 tool_calls = msg.tool_calls or []
1782 if tool_calls:
1783 self.agent_status = "Executing Tools"
1784 self.current_activity = f"Running {len(tool_calls)} tool(s)..."
1786 # First, add the assistant message with tool_calls to session
1787 if content:
1788 session.add_message("assistant", content, tool_calls=tool_calls)
1789 else:
1790 session.add_message("assistant", "", tool_calls=tool_calls)
1792 # Handle tool calls
1793 tool_results = []
1794 for i, tc in enumerate(tool_calls):
1795 fname = tc.function.name
1796 try:
1797 args = json.loads(tc.function.arguments)
1798 except:
1799 args = {} # Fallback if JSON parsing fails
1801 self.current_activity = f"Executing tool {i+1}/{len(tool_calls)}: {fname}"
1803 # Show tool call with animation
1804 tool_panel = Panel(
1805 f"[cyan]Executing: {fname}[/cyan]\n"
1806 f"[dim]Args: {args}[/dim]\n\n"
1807 f"[yellow]Working...[/yellow]",
1808 title=f"Tool {i+1}/{len(tool_calls)}",
1809 border_style="blue"
1810 )
1811 live.update(tool_panel)
1813 tool_start_time = time.time()
1814 try:
1815 # Get the tool function
1816 tool_info = tool_registry.get(fname, {})
1817 if tool_info and "implementation" in tool_info:
1818 func = tool_info["implementation"]
1819 if asyncio.iscoroutinefunction(func):
1820 result = str(await func(**args))
1821 else:
1822 result = str(await asyncio.to_thread(func, **args))
1823 status = "success"
1824 status_icon = "OK"
1825 else:
1826 result = f"Tool '{fname}' not found"
1827 status = "error"
1828 status_icon = "ERROR"
1829 except Exception as e:
1830 result = f"Error: {e}"
1831 status = "error"
1832 status_icon = "ERROR"
1834 execution_time = time.time() - tool_start_time
1836 # Record tool execution
1837 self.tool_history.append({
1838 "tool": fname,
1839 "args": args,
1840 "result": result[:200] + "..." if len(result) > 200 else result,
1841 "status": status,
1842 "time": execution_time,
1843 "timestamp": time.time()
1844 })
1846 # Show tool result
1847 result_panel = Panel(
1848 f"[green]{status_icon} {fname} completed ({execution_time:.1f}s)[/green]\n\n"
1849 f"[white]{result[:500]}{'...' if len(result) > 500 else ''}[/white]",
1850 title=f"Tool Result - {fname}",
1851 border_style="green" if status == "success" else "red"
1852 )
1853 live.update(result_panel)
1855 session.add_tool_output(tc.id, fname, result)
1856 time.sleep(1) # Brief pause to show result
1858 # Continue the conversation
1859 continue
1860 else:
1861 # Final response
1862 self.agent_status = "Responding"
1863 self.current_activity = "Generating final response..."
1865 if content:
1866 # Display response with typing animation
1867 response_panel = Panel(
1868 Markdown(content),
1869 title="[bold cyan]Osiris[/bold cyan]",
1870 border_style="cyan"
1871 )
1872 live.update(response_panel)
1873 session.add_message("assistant", content)
1875 self.agent_status = "Ready"
1876 self.current_activity = ""
1877 break
1879 except Exception as e:
1880 self.agent_status = "Error"
1881 self.current_activity = "Processing failed"
1882 error_panel = Panel(
1883 f"[red]Processing error: {e}[/red]",
1884 title="Error",
1885 border_style="red"
1886 )
1887 console.print(error_panel)
1889 def handle_command(self, command: str):
1890 """Handle slash commands (same as before)"""
1891 parts = command.strip().split()
1892 cmd = parts[0].lower()
1894 if cmd == "/help":
1895 self.show_command_palette()
1896 elif cmd == "/clear":
1897 session.clear()
1898 context.clear()
1899 console.print("[green]🧹 Session and context cleared[/green]")
1900 elif cmd == "/save":
1901 if session.save_session():
1902 console.print("[green]Session saved[/green]")
1903 else:
1904 console.print("[red]Failed to save session[/red]")
1905 elif cmd == "/load":
1906 if session.load_last_session():
1907 console.print("[green]📂 Session loaded[/green]")
1908 else:
1909 console.print("[yellow]⚠️ No previous session found[/yellow]")
1910 elif cmd == "/status":
1911 self.show_status()
1912 elif cmd == "/config":
1913 self.show_config()
1914 elif cmd == "/context":
1915 self.show_context_menu()
1916 elif cmd == "/tools":
1917 self.show_tools_menu()
1918 elif cmd == "/exit":
1919 self.running = False
1920 console.print("[yellow]👋 Goodbye![/yellow]")
1921 else:
1922 console.print(f"[red]❓ Unknown command: {cmd}[/red]")
1923 console.print("[dim]Type /help for available commands[/dim]")
1925 def show_config(self):
1926 """Show current configuration (same as before)"""
1927 config_table = Table(title="Configuration")
1928 config_table.add_column("Setting", style="cyan")
1929 config_table.add_column("Value", style="white")
1931 config_table.add_row("Provider", settings.provider)
1932 config_table.add_row("Model", settings.default_model)
1933 config_table.add_row("Temperature", str(settings.temperature))
1934 config_table.add_row("Max Tokens", "Default")
1935 config_table.add_row("Timeout", f"{settings.timeout}s")
1936 config_table.add_row("Theme", settings.theme)
1937 config_table.add_row("YOLO Mode", "Enabled" if settings.yolo_mode else "Disabled")
1939 console.print(config_table)
1941def run_enhanced_cli():
1942 """Entry point for the enhanced CLI"""
1943 cli = EnhancedOsirisCLI()
1944 cli.run()
1946if __name__ == "__main__":
1947 run_enhanced_cli()