swarm_cli 2.1.13 → 3.0.0.alpha2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/LICENSE +21 -0
  3. data/exe/swarm3 +11 -0
  4. data/lib/swarm_cli/v3/activity_indicator.rb +168 -0
  5. data/lib/swarm_cli/v3/ansi_colors.rb +70 -0
  6. data/lib/swarm_cli/v3/cli.rb +721 -0
  7. data/lib/swarm_cli/v3/command_completer.rb +112 -0
  8. data/lib/swarm_cli/v3/display.rb +607 -0
  9. data/lib/swarm_cli/v3/dropdown.rb +130 -0
  10. data/lib/swarm_cli/v3/event_renderer.rb +161 -0
  11. data/lib/swarm_cli/v3/file_completer.rb +143 -0
  12. data/lib/swarm_cli/v3/raw_input_reader.rb +304 -0
  13. data/lib/swarm_cli/v3/reboot_tool.rb +123 -0
  14. data/lib/swarm_cli/v3/text_input.rb +235 -0
  15. data/lib/swarm_cli/v3.rb +52 -0
  16. metadata +30 -245
  17. data/exe/swarm +0 -6
  18. data/lib/swarm_cli/cli.rb +0 -201
  19. data/lib/swarm_cli/command_registry.rb +0 -61
  20. data/lib/swarm_cli/commands/mcp_serve.rb +0 -130
  21. data/lib/swarm_cli/commands/mcp_tools.rb +0 -148
  22. data/lib/swarm_cli/commands/migrate.rb +0 -55
  23. data/lib/swarm_cli/commands/run.rb +0 -173
  24. data/lib/swarm_cli/config_loader.rb +0 -98
  25. data/lib/swarm_cli/formatters/human_formatter.rb +0 -811
  26. data/lib/swarm_cli/formatters/json_formatter.rb +0 -62
  27. data/lib/swarm_cli/interactive_repl.rb +0 -895
  28. data/lib/swarm_cli/mcp_serve_options.rb +0 -44
  29. data/lib/swarm_cli/mcp_tools_options.rb +0 -59
  30. data/lib/swarm_cli/migrate_options.rb +0 -54
  31. data/lib/swarm_cli/migrator.rb +0 -132
  32. data/lib/swarm_cli/options.rb +0 -151
  33. data/lib/swarm_cli/ui/components/agent_badge.rb +0 -33
  34. data/lib/swarm_cli/ui/components/content_block.rb +0 -120
  35. data/lib/swarm_cli/ui/components/divider.rb +0 -57
  36. data/lib/swarm_cli/ui/components/panel.rb +0 -62
  37. data/lib/swarm_cli/ui/components/usage_stats.rb +0 -70
  38. data/lib/swarm_cli/ui/formatters/cost.rb +0 -49
  39. data/lib/swarm_cli/ui/formatters/number.rb +0 -58
  40. data/lib/swarm_cli/ui/formatters/text.rb +0 -77
  41. data/lib/swarm_cli/ui/formatters/time.rb +0 -73
  42. data/lib/swarm_cli/ui/icons.rb +0 -36
  43. data/lib/swarm_cli/ui/renderers/event_renderer.rb +0 -188
  44. data/lib/swarm_cli/ui/state/agent_color_cache.rb +0 -45
  45. data/lib/swarm_cli/ui/state/depth_tracker.rb +0 -40
  46. data/lib/swarm_cli/ui/state/spinner_manager.rb +0 -170
  47. data/lib/swarm_cli/ui/state/usage_tracker.rb +0 -62
  48. data/lib/swarm_cli/version.rb +0 -5
  49. data/lib/swarm_cli.rb +0 -46
@@ -1,811 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module SwarmCLI
4
- module Formatters
5
- # HumanFormatter creates beautiful, detailed real-time output using reusable UI components.
6
- # Shows everything: agent thinking, tool calls with arguments, results, responses.
7
- # Uses clean component architecture for maintainability and testability.
8
- #
9
- # Modes:
10
- # - :non_interactive - Full headers, task prompt, complete summary (for single execution)
11
- # - :interactive - Minimal output for REPL (headers shown in welcome screen)
12
- class HumanFormatter
13
- attr_reader :spinner_manager
14
-
15
- def initialize(output: $stdout, quiet: false, truncate: false, verbose: false, mode: :non_interactive)
16
- @output = output
17
- @quiet = quiet
18
- @truncate = truncate
19
- @verbose = verbose
20
- @mode = mode
21
-
22
- # Initialize Pastel with TTY detection
23
- @pastel = Pastel.new(enabled: output.tty?)
24
-
25
- # Initialize state managers
26
- @color_cache = SwarmCLI::UI::State::AgentColorCache.new
27
- @depth_tracker = SwarmCLI::UI::State::DepthTracker.new
28
- @usage_tracker = SwarmCLI::UI::State::UsageTracker.new
29
- @spinner_manager = SwarmCLI::UI::State::SpinnerManager.new
30
-
31
- # Initialize components
32
- @divider = SwarmCLI::UI::Components::Divider.new(pastel: @pastel, terminal_width: TTY::Screen.width)
33
- @agent_badge = SwarmCLI::UI::Components::AgentBadge.new(pastel: @pastel, color_cache: @color_cache)
34
- @content_block = SwarmCLI::UI::Components::ContentBlock.new(pastel: @pastel)
35
- @panel = SwarmCLI::UI::Components::Panel.new(pastel: @pastel)
36
-
37
- # Initialize event renderer
38
- @event_renderer = SwarmCLI::UI::Renderers::EventRenderer.new(
39
- pastel: @pastel,
40
- agent_badge: @agent_badge,
41
- depth_tracker: @depth_tracker,
42
- )
43
-
44
- # Track last context percentage for warnings
45
- @last_context_percentage = {}
46
-
47
- # Start time tracking
48
- @start_time = nil
49
- end
50
-
51
- # Called when swarm execution starts
52
- def on_start(config_path:, swarm_name:, lead_agent:, prompt:)
53
- @start_time = Time.now
54
-
55
- # Only show headers in non-interactive mode
56
- if @mode == :non_interactive
57
- print_header(swarm_name, lead_agent)
58
- print_prompt(prompt)
59
- @output.puts @divider.full
60
- @output.puts @pastel.bold("#{SwarmCLI::UI::Icons::INFO} Execution Log:")
61
- @output.puts
62
- end
63
- end
64
-
65
- # Called for each log entry from SwarmSDK
66
- def on_log(entry)
67
- return if @quiet
68
-
69
- case entry[:type]
70
- when "user_prompt"
71
- handle_user_request(entry)
72
- when "agent_step"
73
- handle_agent_step(entry)
74
- when "agent_stop"
75
- handle_agent_stop(entry)
76
- when "tool_call"
77
- handle_tool_call(entry)
78
- when "tool_result"
79
- handle_tool_result(entry)
80
- when "agent_delegation"
81
- handle_agent_delegation(entry)
82
- when "delegation_result"
83
- handle_delegation_result(entry)
84
- when "context_limit_warning"
85
- handle_context_warning(entry)
86
- when "model_lookup_warning"
87
- handle_model_lookup_warning(entry)
88
- when "compression_started"
89
- handle_compression_started(entry)
90
- when "compression_completed"
91
- handle_compression_completed(entry)
92
- when "hook_executed"
93
- handle_hook_executed(entry)
94
- when "breakpoint_enter"
95
- handle_breakpoint_enter(entry)
96
- when "breakpoint_exit"
97
- handle_breakpoint_exit(entry)
98
- when "llm_retry_attempt"
99
- handle_llm_retry_attempt(entry)
100
- when "llm_retry_exhausted"
101
- handle_llm_retry_exhausted(entry)
102
- when "llm_request_failed"
103
- handle_llm_request_failed(entry)
104
- end
105
- end
106
-
107
- # Called when swarm execution completes successfully
108
- def on_success(result:)
109
- # Defensive: ensure all spinners are stopped before showing result
110
- @spinner_manager.stop_all
111
-
112
- if @mode == :non_interactive
113
- # Full result display with summary
114
- @output.puts
115
- @output.puts @divider.full
116
- end
117
-
118
- # Print result (handles mode internally)
119
- print_result(result)
120
-
121
- # Only print summary in non-interactive mode
122
- print_summary(result) if @mode == :non_interactive
123
- end
124
-
125
- # Called when swarm execution fails
126
- def on_error(error:, duration: nil)
127
- # Defensive: ensure all spinners are stopped before showing error
128
- @spinner_manager.stop_all
129
-
130
- @output.puts
131
- @output.puts @divider.full
132
- print_error(error)
133
- @output.puts @divider.full
134
- end
135
-
136
- private
137
-
138
- def handle_user_request(entry)
139
- agent = entry[:agent]
140
- @usage_tracker.track_agent(agent)
141
- @usage_tracker.track_llm_request(entry[:usage])
142
-
143
- # Stop any delegation waiting spinner (in case this agent was delegated to)
144
- unless @quiet
145
- delegation_spinner = "delegation_#{agent}".to_sym
146
- @spinner_manager.stop(delegation_spinner) if @spinner_manager.active?(delegation_spinner)
147
- end
148
-
149
- # Render agent thinking line
150
- @output.puts @event_renderer.agent_thinking(
151
- agent: agent,
152
- model: entry[:model],
153
- timestamp: entry[:timestamp],
154
- )
155
-
156
- # Show tools available
157
- if entry[:tools]&.any?
158
- @output.puts @event_renderer.tools_available(entry[:tools], indent: @depth_tracker.get(agent))
159
- end
160
-
161
- # Show delegation options
162
- if entry[:delegates_to]&.any?
163
- @output.puts @event_renderer.delegates_to(
164
- entry[:delegates_to],
165
- indent: @depth_tracker.get(agent),
166
- color_cache: @color_cache,
167
- )
168
- end
169
-
170
- @output.puts
171
-
172
- # Start spinner for agent thinking
173
- unless @quiet
174
- spinner_key = "agent_#{agent}".to_sym
175
- @spinner_manager.start(spinner_key, "#{agent} is thinking...")
176
- end
177
- end
178
-
179
- def handle_agent_step(entry)
180
- agent = entry[:agent]
181
- indent_level = @depth_tracker.get(agent)
182
-
183
- # Stop agent thinking spinner
184
- unless @quiet
185
- spinner_key = "agent_#{agent}".to_sym
186
- @spinner_manager.stop(spinner_key)
187
- end
188
-
189
- # Track usage
190
- if entry[:usage]
191
- @usage_tracker.track_llm_request(entry[:usage])
192
- @last_context_percentage[agent] = entry[:usage][:tokens_used_percentage]
193
-
194
- # Render usage stats
195
- @output.puts @event_renderer.usage_stats(
196
- tokens: entry[:usage][:total_tokens] || 0,
197
- cost: entry[:usage][:total_cost] || 0.0,
198
- context_pct: entry[:usage][:tokens_used_percentage],
199
- remaining: entry[:usage][:tokens_remaining],
200
- cumulative: entry[:usage][:cumulative_total_tokens],
201
- indent: indent_level,
202
- )
203
- end
204
-
205
- # Display thinking text (if present)
206
- if entry[:content] && !entry[:content].empty?
207
- thinking = @event_renderer.thinking_text(entry[:content], indent: indent_level)
208
- @output.puts thinking unless thinking.empty?
209
- @output.puts if thinking && !thinking.empty?
210
- end
211
-
212
- # Show tool request summary
213
- tool_count = entry[:tool_calls]&.size || 0
214
- if tool_count > 0
215
- indent = @depth_tracker.indent(agent)
216
- @output.puts "#{indent} #{@pastel.dim("→ Requesting #{tool_count} tool#{"s" if tool_count > 1}...")}"
217
- end
218
-
219
- @output.puts
220
- @output.puts @divider.event(indent: indent_level)
221
- end
222
-
223
- def handle_agent_stop(entry)
224
- agent = entry[:agent]
225
- indent_level = @depth_tracker.get(agent)
226
-
227
- # Stop agent thinking spinner with success
228
- unless @quiet
229
- spinner_key = "agent_#{agent}".to_sym
230
- @spinner_manager.success(spinner_key, "completed")
231
- end
232
-
233
- # Track usage
234
- if entry[:usage]
235
- @usage_tracker.track_llm_request(entry[:usage])
236
- @last_context_percentage[agent] = entry[:usage][:tokens_used_percentage]
237
-
238
- # Render usage stats
239
- @output.puts @event_renderer.usage_stats(
240
- tokens: entry[:usage][:total_tokens] || 0,
241
- cost: entry[:usage][:total_cost] || 0.0,
242
- context_pct: entry[:usage][:tokens_used_percentage],
243
- remaining: entry[:usage][:tokens_remaining],
244
- cumulative: entry[:usage][:cumulative_total_tokens],
245
- indent: indent_level,
246
- )
247
- end
248
-
249
- # Display final response (only for top-level agent in non-interactive mode)
250
- # In interactive mode, response is shown by print_result to avoid duplication
251
- if entry[:content] && !entry[:content].empty? && indent_level.zero? && @mode == :non_interactive
252
- @output.puts @event_renderer.agent_response(
253
- agent: agent,
254
- timestamp: entry[:timestamp],
255
- )
256
-
257
- # Render response content
258
- indent = @depth_tracker.indent(agent)
259
- response_lines = entry[:content].split("\n")
260
-
261
- if @truncate && response_lines.length > 12
262
- response_lines.first(12).each { |line| @output.puts "#{indent} #{line}" }
263
- @output.puts "#{indent} #{@pastel.dim("... (#{response_lines.length - 12} more lines)")}"
264
- else
265
- response_lines.each { |line| @output.puts "#{indent} #{line}" }
266
- end
267
- end
268
-
269
- @output.puts
270
- @output.puts @event_renderer.agent_completed(agent: agent)
271
- @output.puts
272
- @output.puts @divider.event(indent: indent_level)
273
- end
274
-
275
- def handle_tool_call(entry)
276
- agent = entry[:agent]
277
- @usage_tracker.track_tool_call(tool_call_id: entry[:tool_call_id], tool_name: entry[:tool])
278
-
279
- # Special handling for Think tool - show as thoughts, not as a tool call
280
- if entry[:tool] == "Think" && entry[:arguments] && entry[:arguments]["thoughts"]
281
- thoughts = entry[:arguments]["thoughts"]
282
- thinking = @event_renderer.thinking_text(thoughts, indent: @depth_tracker.get(agent))
283
- @output.puts thinking unless thinking.empty?
284
- @output.puts
285
- # Don't show spinner for Think tool
286
- return
287
- end
288
-
289
- # Render tool call event
290
- @output.puts @event_renderer.tool_call(
291
- agent: agent,
292
- tool: entry[:tool],
293
- timestamp: entry[:timestamp],
294
- )
295
-
296
- # Show arguments (skip TodoWrite unless verbose)
297
- args = entry[:arguments]
298
- show_args = args && !args.empty?
299
- show_args &&= entry[:tool] != "TodoWrite" || @verbose
300
-
301
- if show_args
302
- @output.puts @event_renderer.tool_arguments(
303
- args,
304
- indent: @depth_tracker.get(agent),
305
- truncate: @truncate,
306
- )
307
- end
308
-
309
- @output.puts
310
-
311
- # Start spinner for tool execution
312
- unless @quiet || entry[:tool] == "TodoWrite"
313
- spinner_key = "tool_#{entry[:tool_call_id]}".to_sym
314
- @spinner_manager.start(spinner_key, "Executing #{entry[:tool]}...")
315
- end
316
- end
317
-
318
- def handle_tool_result(entry)
319
- agent = entry[:agent]
320
- tool_name = entry[:tool] || @usage_tracker.tool_name_for(entry[:tool_call_id])
321
-
322
- # Special handling for Think tool - skip showing result (already shown as thoughts)
323
- if tool_name == "Think"
324
- # Don't show anything - thoughts were already displayed in handle_tool_call
325
- # Start spinner for agent processing
326
- unless @quiet
327
- spinner_key = "agent_#{agent}".to_sym
328
- indent = @depth_tracker.indent(agent)
329
- @spinner_manager.start(spinner_key, "#{indent}#{agent} is processing...")
330
- end
331
- return
332
- end
333
-
334
- # Stop tool spinner with success
335
- unless @quiet || tool_name == "TodoWrite"
336
- spinner_key = "tool_#{entry[:tool_call_id]}".to_sym
337
- @spinner_manager.success(spinner_key, "completed")
338
- end
339
-
340
- # Special handling for TodoWrite
341
- if tool_name == "TodoWrite"
342
- display_todo_list(agent, entry[:timestamp])
343
- else
344
- @output.puts @event_renderer.tool_result(
345
- agent: agent,
346
- timestamp: entry[:timestamp],
347
- tool: tool_name,
348
- )
349
-
350
- # Render result content
351
- if entry[:result].is_a?(String) && !entry[:result].empty?
352
- result_text = @event_renderer.tool_result_content(
353
- entry[:result],
354
- indent: @depth_tracker.get(agent),
355
- truncate: !@verbose,
356
- )
357
- @output.puts result_text unless result_text.empty?
358
- end
359
- end
360
-
361
- @output.puts
362
- @output.puts @divider.event(indent: @depth_tracker.get(agent))
363
-
364
- # Start spinner for agent processing tool result
365
- # The agent will determine what to do next (more tools or finish)
366
- # This spinner will be stopped by the next agent_step or agent_stop event
367
- unless @quiet
368
- spinner_key = "agent_#{agent}".to_sym
369
- indent = @depth_tracker.indent(agent)
370
- @spinner_manager.start(spinner_key, "#{indent}#{agent} is processing...")
371
- end
372
- end
373
-
374
- def handle_agent_delegation(entry)
375
- @usage_tracker.track_tool_call
376
-
377
- @output.puts @event_renderer.delegation(
378
- from: entry[:agent],
379
- to: entry[:delegate_to],
380
- timestamp: entry[:timestamp],
381
- )
382
- @output.puts
383
-
384
- # Show arguments if present
385
- if entry[:arguments] && !entry[:arguments].empty?
386
- @output.puts @event_renderer.tool_arguments(
387
- entry[:arguments],
388
- indent: @depth_tracker.get(entry[:agent]),
389
- truncate: @truncate,
390
- )
391
- end
392
-
393
- @output.puts
394
-
395
- # Start spinner waiting for delegated agent
396
- unless @quiet
397
- spinner_key = "delegation_#{entry[:delegate_to]}".to_sym
398
- indent = @depth_tracker.indent(entry[:agent])
399
- @spinner_manager.start(spinner_key, "#{indent}Waiting for #{entry[:delegate_to]}...")
400
- end
401
- end
402
-
403
- def handle_delegation_result(entry)
404
- @output.puts @event_renderer.delegation_result(
405
- from: entry[:delegate_from],
406
- to: entry[:agent],
407
- timestamp: entry[:timestamp],
408
- )
409
-
410
- # Render result content
411
- if entry[:result].is_a?(String) && !entry[:result].empty?
412
- result_text = @event_renderer.tool_result_content(
413
- entry[:result],
414
- indent: @depth_tracker.get(entry[:agent]),
415
- truncate: !@verbose,
416
- )
417
- @output.puts result_text unless result_text.empty?
418
- end
419
-
420
- @output.puts
421
- @output.puts @divider.event(indent: @depth_tracker.get(entry[:agent]))
422
-
423
- # Start spinner for agent processing delegation result
424
- unless @quiet
425
- spinner_key = "agent_#{entry[:agent]}".to_sym
426
- indent = @depth_tracker.indent(entry[:agent])
427
- @spinner_manager.start(spinner_key, "#{indent}#{entry[:agent]} is processing...")
428
- end
429
- end
430
-
431
- def handle_context_warning(entry)
432
- agent = entry[:agent]
433
- threshold = entry[:threshold]
434
- current_usage = entry[:current_usage]
435
- tokens_remaining = entry[:tokens_remaining]
436
-
437
- # Determine warning severity
438
- type = threshold == "90%" ? :error : :warning
439
-
440
- @output.puts @panel.render(
441
- type: type,
442
- title: "CONTEXT WARNING #{@agent_badge.render(agent)}",
443
- lines: [
444
- @pastel.public_send((type == :error ? :red : :yellow), "Context usage: #{current_usage} (threshold: #{threshold})"),
445
- @pastel.dim("Tokens remaining: #{SwarmCLI::UI::Formatters::Number.format(tokens_remaining)}"),
446
- ],
447
- indent: @depth_tracker.get(agent),
448
- )
449
- end
450
-
451
- def handle_model_lookup_warning(entry)
452
- agent = entry[:agent]
453
- model = entry[:model]
454
- error_message = entry[:error_message]
455
- suggestions = entry[:suggestions] || []
456
-
457
- lines = [
458
- @pastel.yellow("Model '#{model}' not found in registry"),
459
- ]
460
-
461
- if suggestions.any?
462
- lines << @pastel.dim("Did you mean one of these?")
463
- suggestions.each do |suggestion|
464
- model_id = suggestion[:id] || suggestion["id"]
465
- context = suggestion[:context_window] || suggestion["context_window"]
466
- context_display = context ? " (#{SwarmCLI::UI::Formatters::Number.format(context)} tokens)" : ""
467
- lines << " #{@pastel.cyan("•")} #{@pastel.white(model_id)}#{@pastel.dim(context_display)}"
468
- end
469
- else
470
- lines << @pastel.dim("Error: #{error_message}")
471
- end
472
-
473
- lines << @pastel.dim("Context tracking unavailable for this model.")
474
-
475
- @output.puts @panel.render(
476
- type: :warning,
477
- title: "MODEL WARNING #{@agent_badge.render(agent)}",
478
- lines: lines,
479
- indent: 0, # Always at root level (warnings shown at boot, not during execution)
480
- )
481
- end
482
-
483
- def handle_compression_started(entry)
484
- agent = entry[:agent]
485
- message_count = entry[:message_count]
486
- estimated_tokens = entry[:estimated_tokens]
487
-
488
- @output.puts @panel.render(
489
- type: :info,
490
- title: "CONTEXT COMPRESSION #{@agent_badge.render(agent)}",
491
- lines: [
492
- @pastel.dim("Compressing #{message_count} messages (~#{SwarmCLI::UI::Formatters::Number.format(estimated_tokens)} tokens)..."),
493
- ],
494
- indent: @depth_tracker.get(agent),
495
- )
496
- end
497
-
498
- def handle_compression_completed(entry)
499
- agent = entry[:agent]
500
- original_messages = entry[:original_message_count]
501
- compressed_messages = entry[:compressed_message_count]
502
- messages_removed = entry[:messages_removed]
503
- original_tokens = entry[:original_tokens]
504
- compressed_tokens = entry[:compressed_tokens]
505
- compression_ratio = entry[:compression_ratio]
506
- time_taken = entry[:time_taken]
507
-
508
- @output.puts @panel.render(
509
- type: :success,
510
- title: "COMPRESSION COMPLETE #{@agent_badge.render(agent)}",
511
- lines: [
512
- "#{@pastel.dim("Messages:")} #{original_messages} → #{compressed_messages} #{@pastel.green("(-#{messages_removed})")}",
513
- "#{@pastel.dim("Tokens:")} #{SwarmCLI::UI::Formatters::Number.format(original_tokens)} → #{SwarmCLI::UI::Formatters::Number.format(compressed_tokens)} #{@pastel.green("(#{(compression_ratio * 100).round(1)}%)")}",
514
- "#{@pastel.dim("Time taken:")} #{SwarmCLI::UI::Formatters::Time.duration(time_taken)}",
515
- ],
516
- indent: @depth_tracker.get(agent),
517
- )
518
- end
519
-
520
- def handle_hook_executed(entry)
521
- hook_event = entry[:hook_event]
522
- agent = entry[:agent]
523
- success = entry[:success]
524
- blocked = entry[:blocked]
525
- stderr = entry[:stderr]
526
- exit_code = entry[:exit_code]
527
-
528
- @output.puts @event_renderer.hook_executed(
529
- hook_event: hook_event,
530
- agent: agent,
531
- timestamp: entry[:timestamp],
532
- success: success,
533
- blocked: blocked,
534
- )
535
-
536
- # Show stderr if present
537
- if stderr && !stderr.empty?
538
- indent = @depth_tracker.indent(agent)
539
-
540
- if blocked && hook_event == "user_prompt"
541
- @output.puts
542
- @output.puts "#{indent} #{@pastel.bold.red("⛔ Prompt Blocked by Hook:")}"
543
- stderr.lines.each { |line| @output.puts "#{indent} #{@pastel.red(line.chomp)}" }
544
- @output.puts "#{indent} #{@pastel.dim("(Prompt was not sent to the agent)")}"
545
- elsif blocked
546
- @output.puts "#{indent} #{@pastel.red("Blocked:")} #{@pastel.red(stderr)}"
547
- else
548
- @output.puts "#{indent} #{@pastel.yellow("Message:")} #{@pastel.dim(stderr)}"
549
- end
550
- end
551
-
552
- # Show exit code in verbose mode
553
- if @verbose && exit_code
554
- indent = @depth_tracker.indent(agent)
555
- code_color = if exit_code.zero?
556
- :green
557
- else
558
- (exit_code == 2 ? :red : :yellow)
559
- end
560
- @output.puts "#{indent} #{@pastel.dim("Exit code:")} #{@pastel.public_send(code_color, exit_code)}"
561
- end
562
-
563
- @output.puts
564
- end
565
-
566
- def handle_breakpoint_enter(entry)
567
- agent = entry[:agent]
568
- event = entry[:event]
569
-
570
- # Pause all spinners to allow clean interactive debugging
571
- @spinner_manager.pause_all
572
-
573
- # Show debugging notice
574
- @output.puts
575
- @output.puts @pastel.yellow("#{SwarmCLI::UI::Icons::THINKING} Breakpoint: Entering interactive debugging (#{event} hook)")
576
- @output.puts @pastel.dim(" Agent: #{agent}")
577
- @output.puts @pastel.dim(" Type 'exit' to continue execution")
578
- @output.puts
579
- end
580
-
581
- def handle_breakpoint_exit(entry)
582
- # Resume all spinners after debugging
583
- @spinner_manager.resume_all
584
-
585
- @output.puts
586
- @output.puts @pastel.green("#{SwarmCLI::UI::Icons::SUCCESS} Breakpoint: Resuming execution")
587
- @output.puts
588
- end
589
-
590
- def handle_llm_retry_attempt(entry)
591
- agent = entry[:agent]
592
- attempt = entry[:attempt]
593
- max_retries = entry[:max_retries]
594
- error_class = entry[:error_class]
595
- error_message = entry[:error_message]
596
- retry_delay = entry[:retry_delay]
597
-
598
- # Stop agent thinking spinner (if active)
599
- unless @quiet
600
- spinner_key = "agent_#{agent}".to_sym
601
- @spinner_manager.stop(spinner_key) if @spinner_manager.active?(spinner_key)
602
- end
603
-
604
- lines = [
605
- @pastel.yellow("LLM API request failed (attempt #{attempt}/#{max_retries})"),
606
- @pastel.dim("Error: #{error_class}: #{error_message}"),
607
- @pastel.dim("Retrying in #{retry_delay}s..."),
608
- ]
609
-
610
- @output.puts @panel.render(
611
- type: :warning,
612
- title: "RETRY #{@agent_badge.render(agent)}",
613
- lines: lines,
614
- indent: @depth_tracker.get(agent),
615
- )
616
-
617
- # Restart spinner for next attempt
618
- unless @quiet
619
- spinner_key = "agent_#{agent}".to_sym
620
- @spinner_manager.start(spinner_key, "#{agent} is retrying...")
621
- end
622
- end
623
-
624
- def handle_llm_retry_exhausted(entry)
625
- agent = entry[:agent]
626
- attempts = entry[:attempts]
627
- error_class = entry[:error_class]
628
- error_message = entry[:error_message]
629
-
630
- # Stop agent thinking spinner (if active)
631
- unless @quiet
632
- spinner_key = "agent_#{agent}".to_sym
633
- @spinner_manager.stop(spinner_key) if @spinner_manager.active?(spinner_key)
634
- end
635
-
636
- lines = [
637
- @pastel.red("LLM API request failed after #{attempts} attempts"),
638
- @pastel.dim("Error: #{error_class}: #{error_message}"),
639
- @pastel.dim("No more retries available"),
640
- ]
641
-
642
- @output.puts @panel.render(
643
- type: :error,
644
- title: "RETRY EXHAUSTED #{@agent_badge.render(agent)}",
645
- lines: lines,
646
- indent: @depth_tracker.get(agent),
647
- )
648
- end
649
-
650
- def handle_llm_request_failed(entry)
651
- agent = entry[:agent]
652
- entry[:error_type]
653
- error_class = entry[:error_class]
654
- error_message = entry[:error_message]
655
- status_code = entry[:status_code]
656
-
657
- # Stop agent thinking spinner (if active)
658
- unless @quiet
659
- spinner_key = "agent_#{agent}".to_sym
660
- @spinner_manager.stop(spinner_key) if @spinner_manager.active?(spinner_key)
661
- end
662
-
663
- status_display = status_code ? " (#{status_code})" : ""
664
- lines = [
665
- @pastel.red("LLM request failed#{status_display}"),
666
- @pastel.dim("Error: #{error_class}: #{error_message}"),
667
- @pastel.dim("This error cannot be automatically recovered"),
668
- ]
669
-
670
- @output.puts @panel.render(
671
- type: :error,
672
- title: "REQUEST FAILED #{@agent_badge.render(agent)}",
673
- lines: lines,
674
- indent: @depth_tracker.get(agent),
675
- )
676
- end
677
-
678
- def display_todo_list(agent, timestamp)
679
- todos = SwarmSDK::Tools::Stores::TodoManager.get_todos(agent.to_sym)
680
- indent = @depth_tracker.indent(agent)
681
- time = SwarmCLI::UI::Formatters::Time.timestamp(timestamp)
682
-
683
- if todos.empty?
684
- @output.puts "#{indent}#{@pastel.dim(time)} #{@pastel.cyan("#{SwarmCLI::UI::Icons::BULLET} Todo list")} updated (empty)"
685
- return
686
- end
687
-
688
- @output.puts "#{indent}#{@pastel.dim(time)} #{@pastel.cyan("#{SwarmCLI::UI::Icons::BULLET} Todo list")} updated:"
689
- @output.puts
690
-
691
- todos.each_with_index do |todo, index|
692
- status = todo[:status] || todo["status"]
693
- content = todo[:content] || todo["content"]
694
- num = index + 1
695
-
696
- line = case status
697
- when "completed"
698
- "#{indent} #{@pastel.dim("#{num}.")} #{@pastel.dim.strikethrough(content)}"
699
- when "in_progress"
700
- "#{indent} #{@pastel.bold.yellow("#{num}.")} #{@pastel.bold(content)}"
701
- when "pending"
702
- "#{indent} #{@pastel.white("#{num}.")} #{content}"
703
- else
704
- "#{indent} #{num}. #{content}"
705
- end
706
-
707
- @output.puts line
708
- end
709
- end
710
-
711
- def print_header(swarm_name, lead_agent)
712
- @output.puts
713
- @output.puts @pastel.bold.bright_cyan("#{SwarmCLI::UI::Icons::SPARKLES} SwarmSDK - AI Agent Orchestration #{SwarmCLI::UI::Icons::SPARKLES}")
714
- @output.puts @divider.full
715
- @output.puts "#{@pastel.bold("Swarm:")} #{@pastel.cyan(swarm_name)}"
716
- @output.puts "#{@pastel.bold("Lead Agent:")} #{@pastel.cyan(lead_agent)}"
717
- @output.puts
718
- end
719
-
720
- def print_prompt(prompt)
721
- @output.puts @pastel.bold("#{SwarmCLI::UI::Icons::THINKING} Task Prompt:")
722
- @output.puts @pastel.bright_white(prompt)
723
- @output.puts
724
- end
725
-
726
- def print_result(result)
727
- return unless result.content && !result.content.empty?
728
-
729
- # Interactive mode: Just show the response content directly
730
- if @mode == :interactive
731
- # Render markdown if content looks like markdown
732
- content_to_display = if looks_like_markdown?(result.content)
733
- begin
734
- TTY::Markdown.parse(result.content)
735
- rescue StandardError
736
- result.content
737
- end
738
- else
739
- result.content
740
- end
741
-
742
- @output.puts content_to_display
743
- @output.puts
744
- return
745
- end
746
-
747
- # Non-interactive mode: Full result display with header and dividers
748
- @output.puts
749
- @output.puts @pastel.bold.green("#{SwarmCLI::UI::Icons::SUCCESS} Execution Complete")
750
- @output.puts
751
- @output.puts @pastel.bold("#{SwarmCLI::UI::Icons::RESPONSE} Final Response from #{@agent_badge.render(result.agent)}:")
752
- @output.puts
753
- @output.puts @divider.full
754
-
755
- # Render markdown if content looks like markdown
756
- content_to_display = if looks_like_markdown?(result.content)
757
- begin
758
- TTY::Markdown.parse(result.content)
759
- rescue StandardError
760
- result.content
761
- end
762
- else
763
- result.content
764
- end
765
-
766
- @output.puts content_to_display
767
- @output.puts @divider.full
768
- @output.puts
769
- end
770
-
771
- def print_summary(result)
772
- @output.puts @pastel.bold("#{SwarmCLI::UI::Icons::INFO} Execution Summary:")
773
- @output.puts
774
-
775
- # Agents used (colored list)
776
- agents_display = @agent_badge.render_list(@usage_tracker.agents)
777
- @output.puts " #{SwarmCLI::UI::Icons::AGENT} #{@pastel.bold("Agents used:")} #{agents_display}"
778
-
779
- # Metrics
780
- @output.puts " #{SwarmCLI::UI::Icons::LLM} #{@pastel.bold("LLM Requests:")} #{result.llm_requests}"
781
- @output.puts " #{SwarmCLI::UI::Icons::TOOL} #{@pastel.bold("Tool Calls:")} #{result.tool_calls_count}"
782
- @output.puts " #{SwarmCLI::UI::Icons::TOKENS} #{@pastel.bold("Total Tokens:")} #{SwarmCLI::UI::Formatters::Number.format(result.total_tokens)}"
783
- @output.puts " #{SwarmCLI::UI::Icons::COST} #{@pastel.bold("Total Cost:")} #{SwarmCLI::UI::Formatters::Cost.format(result.total_cost, pastel: @pastel)}"
784
- @output.puts " #{SwarmCLI::UI::Icons::TIME} #{@pastel.bold("Duration:")} #{SwarmCLI::UI::Formatters::Time.duration(result.duration)}"
785
-
786
- @output.puts
787
- end
788
-
789
- def print_error(error)
790
- @output.puts
791
- @output.puts @pastel.bold.red("#{SwarmCLI::UI::Icons::ERROR} Execution Failed")
792
- @output.puts
793
- @output.puts @pastel.red("Error: #{error.class.name}")
794
- @output.puts @pastel.red(error.message)
795
- @output.puts
796
-
797
- return unless error.backtrace
798
-
799
- @output.puts @pastel.dim("Backtrace:")
800
- error.backtrace.first(5).each do |line|
801
- @output.puts @pastel.dim(" #{line}")
802
- end
803
- @output.puts
804
- end
805
-
806
- def looks_like_markdown?(text)
807
- text.match?(/^#+\s|^\*\s|^-\s|^\d+\.\s|```|\[.+\]\(.+\)/)
808
- end
809
- end
810
- end
811
- end