Class: SwarmCLI::InteractiveREPL
- Inherits:
-
Object
- Object
- SwarmCLI::InteractiveREPL
- Defined in:
- lib/swarm_cli/interactive_repl.rb
Overview
InteractiveREPL provides a professional, interactive terminal interface for conversing with SwarmSDK agents.
Features:
- Multiline input with intuitive submission (Enter on empty line or Ctrl+D)
- Beautiful Markdown rendering for agent responses
- Progress indicators during processing
- Command system (/help, /exit, /clear, etc.)
- Conversation history with context preservation
- Professional styling with Pastel and TTY tools
Constant Summary collapse
- COMMANDS =
{ "/help" => "Show available commands", "/clear" => "Clear the lead agent's conversation context", "/tools" => "List the lead agent's available tools", "/history" => "Show conversation history", "/defrag" => "Run memory defragmentation workflow (find and link related entries)", "/exit" => "Exit the REPL (or press Ctrl+D)", }.freeze
- HISTORY_SIZE =
History configuration
1000
Class Method Summary collapse
-
.history_file ⇒ Object
Get history file path (can be overridden with SWARM_HISTORY env var).
Instance Method Summary collapse
-
#execute_with_cancellation(input, &log_callback) ⇒ SwarmSDK::Result?
Execute a message with Ctrl+C cancellation support Public for testing.
-
#handle_command(input) ⇒ Object
Handle slash commands Public for testing.
-
#initialize(swarm:, options:, initial_message: nil) ⇒ InteractiveREPL
constructor
A new instance of InteractiveREPL.
- #run ⇒ Object
-
#save_persistent_history ⇒ void
Save persistent history to file Public for testing.
Constructor Details
#initialize(swarm:, options:, initial_message: nil) ⇒ InteractiveREPL
Returns a new instance of InteractiveREPL.
43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
# File 'lib/swarm_cli/interactive_repl.rb', line 43 def initialize(swarm:, options:, initial_message: nil) @swarm = swarm @options = @initial_message = @conversation_history = [] @session_results = [] # Accumulate all results for session summary @validation_warnings_shown = false setup_ui_components setup_persistent_history # Create formatter for swarm execution output (interactive mode) @formatter = Formatters::HumanFormatter.new( output: $stdout, quiet: .quiet?, truncate: .truncate?, verbose: .verbose?, mode: :interactive, ) end |
Class Method Details
.history_file ⇒ Object
Get history file path (can be overridden with SWARM_HISTORY env var)
38 39 40 |
# File 'lib/swarm_cli/interactive_repl.rb', line 38 def history_file ENV["SWARM_HISTORY"] || File.("~/.swarm/history") end |
Instance Method Details
#execute_with_cancellation(input, &log_callback) ⇒ SwarmSDK::Result?
Execute a message with Ctrl+C cancellation support Public for testing
96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 |
# File 'lib/swarm_cli/interactive_repl.rb', line 96 def execute_with_cancellation(input, &log_callback) cancelled = false result = nil # Execute in Async block to enable Ctrl+C cancellation Async do |task| # Use Async::Condition for trap-safe cancellation # (Condition#signal uses Thread::Queue which is safe from trap context) cancel_condition = Async::Condition.new # Install trap ONLY during execution # When Ctrl+C is pressed, signal the condition instead of calling task.stop old_trap = trap("INT") do cancel_condition.signal(:cancel) end begin # Execute swarm in async task llm_task = task.async do @swarm.execute(input, &log_callback) end # Monitor task - watches for cancellation signal # Must be created AFTER llm_task so it can reference it monitor_task = task.async do if cancel_condition.wait == :cancel cancelled = true llm_task.stop end end result = llm_task.wait rescue Async::Stop # Task was stopped by Ctrl+C cancelled = true ensure # Clean up monitor task monitor_task&.stop if monitor_task&.alive? # CRITICAL: Restore old trap when done # This ensures Ctrl+C at the prompt still exits the REPL trap("INT", old_trap) end end.wait cancelled ? nil : result end |
#handle_command(input) ⇒ Object
Handle slash commands Public for testing
148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 |
# File 'lib/swarm_cli/interactive_repl.rb', line 148 def handle_command(input) command = input.split.first.downcase case command when "/help" display_help when "/clear" clear_context when "/tools" list_tools when "/history" display_history when "/defrag" defrag_memory when "/exit" # Break from main loop to trigger session summary throw(:exit_repl) else puts render_error("Unknown command: #{command}") puts @colors[:system].call("Type /help for available commands") end end |
#run ⇒ Object
64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 |
# File 'lib/swarm_cli/interactive_repl.rb', line 64 def run display_welcome # Emit validation warnings before first prompt emit_validation_warnings_before_prompt # Send initial message if provided if @initial_message && !@initial_message.empty? (@initial_message) end main_loop display_goodbye display_session_summary rescue Interrupt puts "\n" display_goodbye display_session_summary exit(130) ensure # Defensive: ensure all spinners are stopped on exit @formatter&.spinner_manager&.stop_all # Save history on exit save_persistent_history end |
#save_persistent_history ⇒ void
This method returns an undefined value.
Save persistent history to file Public for testing
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 |
# File 'lib/swarm_cli/interactive_repl.rb', line 175 def save_persistent_history history_file = self.class.history_file return unless history_file history = Reline::HISTORY.to_a # Limit to configured size if HISTORY_SIZE.positive? && history.size > HISTORY_SIZE history = history.last(HISTORY_SIZE) end # Write with secure permissions (owner read/write only) File.open(history_file, "w", 0o600, encoding: Encoding::UTF_8) do |f| # Handle multi-line entries by escaping newlines with backslash history.each do |entry| escaped = entry.scrub.split("\n").join("\\\n") f.puts(escaped) end end rescue Errno::EACCES, Errno::ENOENT # Can't write history - continue anyway nil end |