swarm_cli 2.0.1 → 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: f57ce5f511e43f8c520bf787d993cdcff34f3152870d496f72b5bbdee962c1a5
4
- data.tar.gz: e21157d97dfce0fc71806683b6cb755c0e5ce9901c43c71764aab2b398d23fb1
3
+ metadata.gz: d5f6285a136ccf9bda4e010daf5062f38d2dc1fbe27ef0747dd7e08a0f794bfc
4
+ data.tar.gz: 54400709e283dce60b2422392a45f3ccc56b0876b41ccd919a17b12efbd19413
5
5
  SHA512:
6
- metadata.gz: e8702078d6573711ebe3c08c74571c0ce3d6b1de19810fa324cf6a38122573c7268c906b35e9f3446eb812dbad7d06ce4b61a01e24ff4c2ba5d56244f2b659d0
7
- data.tar.gz: 252d1ee2ad3acb39f4302a558ec8633a5eba854522b6924362ecbee4db9f0dfdfe92020a531333d615f1402879b3d2ea8ed9ee4ee140ec668c59a6ad5254f20e
6
+ metadata.gz: afbaa1d360f97e20167256ebfa6c6c68d377bac849a295197641443c7a59f306a132a1084a5bf89d8490b9d79dce67f4bb0a1b480642ce68dedeb758af0ef67e
7
+ data.tar.gz: b37a53f4855db437e74d2fd6a60245650a41496c603f5529371f5471a4e7d7889bd34f11c5b3764ddc0ae3f97a20777dc9a249da4659c11155a7e5ab7d7f29f8
@@ -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"),
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SwarmCLI
4
- VERSION = "2.0.1"
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.1
4
+ version: 2.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paulo Arruda