swarm_cli 2.0.0 → 2.0.2

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c76f2194c98896ffdd5f1c16a08e0ce0460d3430efc9c4f21adc4999c1af73f7
4
- data.tar.gz: c004295295b56474ba2eafad610fb2804ae9e8d171d1103b08faa5d7e93881a0
3
+ metadata.gz: d5f6285a136ccf9bda4e010daf5062f38d2dc1fbe27ef0747dd7e08a0f794bfc
4
+ data.tar.gz: 54400709e283dce60b2422392a45f3ccc56b0876b41ccd919a17b12efbd19413
5
5
  SHA512:
6
- metadata.gz: 8203e59b81a125b4d1ae7188e3a90e9158c228882e15237be1475e64a9ace7489ad0879f8a6507cdd1d0fba7f09ed448e574cbe5df424727f0ddafd06e4d4bd3
7
- data.tar.gz: f433f4105c2adeec9e37b8d0539a81180ba5d66d0b979529acd604906fecb1c575f80c352c6620ce89faff6300d0e4ab40b76714c8af71b11e87f25f494da81e
6
+ metadata.gz: afbaa1d360f97e20167256ebfa6c6c68d377bac849a295197641443c7a59f306a132a1084a5bf89d8490b9d79dce67f4bb0a1b480642ce68dedeb758af0ef67e
7
+ data.tar.gz: b37a53f4855db437e74d2fd6a60245650a41496c603f5529371f5471a4e7d7889bd34f11c5b3764ddc0ae3f97a20777dc9a249da4659c11155a7e5ab7d7f29f8
data/lib/swarm_cli/cli.rb CHANGED
@@ -4,7 +4,6 @@ module SwarmCLI
4
4
  class CLI
5
5
  class << self
6
6
  def start(args)
7
- SwarmSDK.refresh_models_silently
8
7
  new(args).run
9
8
  end
10
9
  end
@@ -14,7 +14,9 @@ module SwarmCLI
14
14
 
15
15
  def initialize(options)
16
16
  @options = options
17
- @scratchpad = SwarmSDK::Scratchpad.new
17
+ # Create scratchpad with persistence for MCP server
18
+ scratchpad_path = File.join(Dir.pwd, ".swarm", "scratchpad.json")
19
+ @scratchpad = SwarmSDK::Scratchpad.new(persist_to: scratchpad_path)
18
20
  end
19
21
 
20
22
  def execute
@@ -10,6 +10,8 @@ module SwarmCLI
10
10
  # - :non_interactive - Full headers, task prompt, complete summary (for single execution)
11
11
  # - :interactive - Minimal output for REPL (headers shown in welcome screen)
12
12
  class HumanFormatter
13
+ attr_reader :spinner_manager
14
+
13
15
  def initialize(output: $stdout, quiet: false, truncate: false, verbose: false, mode: :non_interactive)
14
16
  @output = output
15
17
  @quiet = quiet
@@ -5,6 +5,8 @@ require "tty-spinner"
5
5
  require "tty-markdown"
6
6
  require "tty-box"
7
7
  require "pastel"
8
+ require "async"
9
+ require "async/condition"
8
10
 
9
11
  module SwarmCLI
10
12
  # InteractiveREPL provides a professional, interactive terminal interface
@@ -67,6 +69,59 @@ module SwarmCLI
67
69
  exit(130)
68
70
  end
69
71
 
72
+ # Execute a message with Ctrl+C cancellation support
73
+ # Public for testing
74
+ #
75
+ # @param input [String] User input to execute
76
+ # @return [SwarmSDK::Result, nil] Result or nil if cancelled
77
+ def execute_with_cancellation(input, &log_callback)
78
+ cancelled = false
79
+ result = nil
80
+
81
+ # Execute in Async block to enable Ctrl+C cancellation
82
+ Async do |task|
83
+ # Use Async::Condition for trap-safe cancellation
84
+ # (Condition#signal uses Thread::Queue which is safe from trap context)
85
+ cancel_condition = Async::Condition.new
86
+
87
+ # Install trap ONLY during execution
88
+ # When Ctrl+C is pressed, signal the condition instead of calling task.stop
89
+ old_trap = trap("INT") do
90
+ cancel_condition.signal(:cancel)
91
+ end
92
+
93
+ begin
94
+ # Execute swarm in async task
95
+ llm_task = task.async do
96
+ @swarm.execute(input, &log_callback)
97
+ end
98
+
99
+ # Monitor task - watches for cancellation signal
100
+ # Must be created AFTER llm_task so it can reference it
101
+ monitor_task = task.async do
102
+ if cancel_condition.wait == :cancel
103
+ cancelled = true
104
+ llm_task.stop
105
+ end
106
+ end
107
+
108
+ result = llm_task.wait
109
+ rescue Async::Stop
110
+ # Task was stopped by Ctrl+C
111
+ cancelled = true
112
+ ensure
113
+ # Clean up monitor task
114
+ monitor_task&.stop if monitor_task&.alive?
115
+
116
+ # CRITICAL: Restore old trap when done
117
+ # This ensures Ctrl+C at the prompt still exits the REPL
118
+ trap("INT", old_trap)
119
+ end
120
+ end.wait
121
+
122
+ cancelled ? nil : result
123
+ end
124
+
70
125
  private
71
126
 
72
127
  def setup_ui_components
@@ -127,6 +182,7 @@ module SwarmCLI
127
182
  puts @colors[:system].call("Lead Agent: #{@swarm.lead_agent}")
128
183
  puts ""
129
184
  puts @colors[:system].call("Type your message and press Enter to submit")
185
+ puts @colors[:system].call("Press Option+Enter (or ESC then Enter) for multi-line input")
130
186
  puts @colors[:system].call("Type #{@colors[:code].call("/help")} for commands or #{@colors[:code].call("/exit")} to quit")
131
187
  puts ""
132
188
  puts divider
@@ -159,14 +215,17 @@ module SwarmCLI
159
215
  # Build the prompt indicator with colors
160
216
  prompt_indicator = build_prompt_indicator
161
217
 
162
- # Use Reline for flicker-free input (same as IRB)
218
+ # Use Reline.readmultiline for multi-line input support
219
+ # - Option+ENTER (or ESC+ENTER): Adds a newline, continues editing
220
+ # - Regular ENTER: Always submits immediately
163
221
  # Second parameter true = add to history for arrow up/down
164
- line = Reline.readline(prompt_indicator, true)
222
+ # Block always returns true = ENTER always submits
223
+ input = Reline.readmultiline(prompt_indicator, true) { |_lines| true }
165
224
 
166
- return if line.nil? # Ctrl+D returns nil
225
+ return if input.nil? # Ctrl+D returns nil
167
226
 
168
- # Reline doesn't include newline, just strip whitespace
169
- line.strip
227
+ # Strip whitespace from the complete input
228
+ input.strip
170
229
  end
171
230
 
172
231
  def display_prompt_stats
@@ -279,8 +338,8 @@ module SwarmCLI
279
338
 
280
339
  puts ""
281
340
 
282
- # Execute swarm with logging through formatter
283
- result = @swarm.execute(input) do |log_entry|
341
+ # Execute with cancellation support
342
+ result = execute_with_cancellation(input) do |log_entry|
284
343
  # Skip model warnings - already emitted before first prompt
285
344
  next if log_entry[:type] == "model_lookup_warning"
286
345
 
@@ -292,6 +351,17 @@ module SwarmCLI
292
351
  end
293
352
  end
294
353
 
354
+ # Handle cancellation (result is nil when cancelled)
355
+ if result.nil?
356
+ # Stop all active spinners
357
+ @formatter.spinner_manager.stop_all
358
+
359
+ puts ""
360
+ puts @colors[:warning].call("✗ Request cancelled by user")
361
+ puts ""
362
+ return
363
+ end
364
+
295
365
  # Check for errors
296
366
  if result.failure?
297
367
  @formatter.on_error(error: result.error, duration: result.duration)
@@ -353,7 +423,9 @@ module SwarmCLI
353
423
  end,
354
424
  "",
355
425
  @colors[:system].call("Input Tips:"),
356
- @colors[:system].call(" • Type your message and press Enter to submit"),
426
+ @colors[:system].call(" • Press Enter to submit your message"),
427
+ @colors[:system].call(" • Press Option+Enter (or ESC then Enter) for multi-line input"),
428
+ @colors[:system].call(" • Press Ctrl+C to cancel an ongoing request"),
357
429
  @colors[:system].call(" • Press Ctrl+D to exit"),
358
430
  @colors[:system].call(" • Use arrow keys for history and editing"),
359
431
  @colors[:system].call(" • Type / for commands or @ for file paths"),
@@ -496,6 +568,23 @@ module SwarmCLI
496
568
  # Capture COMMANDS for use in lambda
497
569
  commands = COMMANDS
498
570
 
571
+ # Capture file completion logic for use in lambda (since lambda runs in different context)
572
+ file_completions = lambda do |target|
573
+ has_at_prefix = target.start_with?("@")
574
+ query = has_at_prefix ? target[1..] : target
575
+
576
+ next Dir.glob("*").sort.first(20) if query.empty?
577
+
578
+ # Find files matching query anywhere in path
579
+ pattern = "**/*#{query}*"
580
+ found = Dir.glob(pattern, File::FNM_CASEFOLD).reject do |path|
581
+ path.split("/").any? { |part| part.start_with?(".") }
582
+ end.sort.first(20)
583
+
584
+ # Add @ prefix if needed
585
+ has_at_prefix ? found.map { |p| "@#{p}" } : found
586
+ end
587
+
499
588
  # Custom dialog proc for fuzzy file/command completion
500
589
  fuzzy_proc = lambda do
501
590
  # State: [pre, target, post, matches, pointer, navigating]
@@ -503,6 +592,20 @@ module SwarmCLI
503
592
  # Check if this is a navigation key press
504
593
  is_nav_key = key&.match?(dialog.name)
505
594
 
595
+ # If we were in navigation mode and user typed a regular key (not Tab), exit nav mode
596
+ if !context.empty? && context.size >= 6 && context[5] && !is_nav_key
597
+ context[5] = false # Exit navigation mode
598
+ end
599
+
600
+ # Early check: if user typed and current target has spaces, close dialog
601
+ unless is_nav_key || context.empty?
602
+ _, target_check, = retrieve_completion_block
603
+ if target_check.include?(" ")
604
+ context.clear
605
+ return
606
+ end
607
+ end
608
+
506
609
  # Detect if we should recalculate matches
507
610
  should_recalculate = if context.empty?
508
611
  true # First time - initialize
@@ -529,8 +632,8 @@ module SwarmCLI
529
632
  query.empty? || cmd.downcase.include?(query.downcase)
530
633
  end.sort
531
634
  elsif target.start_with?("@") || target.include?("/")
532
- # File path completions - extract to reduce nesting
533
- get_file_completions(target)
635
+ # File path completions - use captured lambda
636
+ file_completions.call(target)
534
637
  end
535
638
 
536
639
  return if matches.nil? || matches.empty?
@@ -605,23 +708,5 @@ module SwarmCLI
605
708
  # Register the custom fuzzy dialog
606
709
  Reline.add_dialog_proc(:fuzzy_complete, fuzzy_proc, [])
607
710
  end
608
-
609
- # Get file path completions with fuzzy matching
610
- # Extracted to reduce block nesting in the dialog lambda
611
- def get_file_completions(target)
612
- has_at_prefix = target.start_with?("@")
613
- query = has_at_prefix ? target[1..] : target
614
-
615
- return Dir.glob("*").sort.first(20) if query.empty?
616
-
617
- # Find files matching query anywhere in path
618
- pattern = "**/*#{query}*"
619
- found = Dir.glob(pattern, File::FNM_CASEFOLD).reject do |path|
620
- path.split("/").any? { |part| part.start_with?(".") }
621
- end.sort.first(20)
622
-
623
- # Add @ prefix if needed
624
- has_at_prefix ? found.map { |p| "@#{p}" } : found
625
- end
626
711
  end
627
712
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmCLI
4
- VERSION = "2.0.0"
4
+ VERSION = "2.0.2"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: swarm_cli
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.0.0
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda