openclacky 0.5.5 → 0.6.0

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/.clackyrules +4 -0
  3. data/CHANGELOG.md +43 -0
  4. data/README.md +1 -1
  5. data/docs/ui2-architecture.md +124 -0
  6. data/lib/clacky/agent.rb +354 -296
  7. data/lib/clacky/agent_config.rb +1 -7
  8. data/lib/clacky/cli.rb +157 -330
  9. data/lib/clacky/client.rb +68 -36
  10. data/lib/clacky/gitignore_parser.rb +26 -12
  11. data/lib/clacky/model_pricing.rb +6 -2
  12. data/lib/clacky/progress_indicator.rb +1 -1
  13. data/lib/clacky/session_manager.rb +6 -2
  14. data/lib/clacky/tools/file_reader.rb +73 -10
  15. data/lib/clacky/tools/glob.rb +65 -9
  16. data/lib/clacky/tools/grep.rb +44 -116
  17. data/lib/clacky/tools/run_project.rb +5 -0
  18. data/lib/clacky/tools/safe_shell.rb +49 -13
  19. data/lib/clacky/tools/shell.rb +1 -49
  20. data/lib/clacky/tools/web_fetch.rb +2 -2
  21. data/lib/clacky/tools/web_search.rb +38 -26
  22. data/lib/clacky/ui2/README.md +214 -0
  23. data/lib/clacky/ui2/components/base_component.rb +163 -0
  24. data/lib/clacky/ui2/components/common_component.rb +89 -0
  25. data/lib/clacky/ui2/components/inline_input.rb +187 -0
  26. data/lib/clacky/ui2/components/input_area.rb +1029 -0
  27. data/lib/clacky/ui2/components/message_component.rb +76 -0
  28. data/lib/clacky/ui2/components/output_area.rb +112 -0
  29. data/lib/clacky/ui2/components/todo_area.rb +137 -0
  30. data/lib/clacky/ui2/components/tool_component.rb +106 -0
  31. data/lib/clacky/ui2/components/welcome_banner.rb +93 -0
  32. data/lib/clacky/ui2/layout_manager.rb +331 -0
  33. data/lib/clacky/ui2/line_editor.rb +201 -0
  34. data/lib/clacky/ui2/screen_buffer.rb +238 -0
  35. data/lib/clacky/ui2/theme_manager.rb +68 -0
  36. data/lib/clacky/ui2/themes/base_theme.rb +99 -0
  37. data/lib/clacky/ui2/themes/hacker_theme.rb +56 -0
  38. data/lib/clacky/ui2/themes/minimal_theme.rb +50 -0
  39. data/lib/clacky/ui2/ui_controller.rb +720 -0
  40. data/lib/clacky/ui2/view_renderer.rb +160 -0
  41. data/lib/clacky/ui2.rb +37 -0
  42. data/lib/clacky/utils/file_ignore_helper.rb +126 -0
  43. data/lib/clacky/version.rb +1 -1
  44. data/lib/clacky.rb +1 -6
  45. metadata +38 -6
  46. data/lib/clacky/ui/banner.rb +0 -155
  47. data/lib/clacky/ui/enhanced_prompt.rb +0 -540
  48. data/lib/clacky/ui/formatter.rb +0 -209
  49. data/lib/clacky/ui/statusbar.rb +0 -96
@@ -0,0 +1,214 @@
1
+ # Clacky UI2 - MVC Terminal UI System
2
+
3
+ A modern, MVC-based terminal UI system with split-screen layout, component-based rendering, and direct method calls for simplicity.
4
+
5
+ ## Features
6
+
7
+ - **Split-Screen Layout**: Scrollable output area on top, fixed input area at bottom
8
+ - **MVC Architecture**: Clean separation of concerns (Model-View-Controller)
9
+ - **Direct Method Calls**: Agent directly calls UIController semantic methods
10
+ - **Component-Based**: Reusable, composable UI components
11
+ - **Scrollable Output**: Navigate through history with arrow keys
12
+ - **Input History**: Navigate previous inputs with up/down arrows
13
+ - **Responsive**: Handles terminal resize automatically
14
+ - **Rich Formatting**: Colored output with Pastel integration
15
+
16
+ ## Architecture
17
+
18
+ ```
19
+ +---------------------------------------------+
20
+ | Business Layer (Agent) |
21
+ | Agent directly calls @ui.show_xxx() |
22
+ +-----------------------+---------------------+
23
+ | Direct calls
24
+ v
25
+ +---------------------------------------------+
26
+ | Controller Layer (UIController) |
27
+ | - show_assistant_message() |
28
+ | - show_tool_call() |
29
+ | - show_tool_result() |
30
+ | - request_confirmation() |
31
+ +-----------------------+---------------------+
32
+ | Render
33
+ v
34
+ +---------------------------------------------+
35
+ | View Layer (ViewRenderer) |
36
+ | - Components (Message, Tool, Status) |
37
+ +---------------------------------------------+
38
+ ```
39
+
40
+ ## Quick Start
41
+
42
+ ### Agent Integration
43
+
44
+ ```ruby
45
+ # Create UI controller
46
+ ui_controller = Clacky::UI2::UIController.new(
47
+ working_dir: Dir.pwd,
48
+ mode: "confirm_safes",
49
+ model: "claude-3-5-sonnet"
50
+ )
51
+
52
+ # Create agent with UI injected
53
+ agent = Clacky::Agent.new(client, config, ui: ui_controller)
54
+
55
+ # Set up input handler
56
+ ui_controller.on_input do |input, images|
57
+ result = agent.run(input, images: images)
58
+ end
59
+
60
+ # Start UI (blocks until exit)
61
+ ui_controller.start
62
+ ```
63
+
64
+ ### UIController Semantic Methods
65
+
66
+ The Agent calls these methods directly:
67
+
68
+ ```ruby
69
+ # Show messages
70
+ @ui.show_assistant_message("Hello!")
71
+ @ui.show_user_message("Hi there", images: [])
72
+
73
+ # Show tool operations
74
+ @ui.show_tool_call("file_reader", { path: "test.rb" })
75
+ @ui.show_tool_result("File contents...")
76
+ @ui.show_tool_error("File not found")
77
+
78
+ # Show status
79
+ @ui.show_progress("Thinking...")
80
+ @ui.clear_progress
81
+ @ui.show_complete(iterations: 5, cost: 0.001)
82
+
83
+ # Show info/warning/error
84
+ @ui.show_info("Session saved")
85
+ @ui.show_warning("Rate limited")
86
+ @ui.show_error("Failed to connect")
87
+
88
+ # Interactive confirmation
89
+ result = @ui.request_confirmation("Allow file write?", default: true)
90
+ # Returns: true/false for yes/no, String for feedback, nil for cancelled
91
+
92
+ # Show diff
93
+ @ui.show_diff(old_content, new_content, max_lines: 50)
94
+ ```
95
+
96
+ ## Components
97
+
98
+ ### ViewRenderer
99
+
100
+ Unified interface for rendering all UI components.
101
+
102
+ ```ruby
103
+ renderer = Clacky::UI2::ViewRenderer.new
104
+
105
+ # Render messages
106
+ renderer.render_user_message("Hello")
107
+ renderer.render_assistant_message("Hi there")
108
+
109
+ # Render tools
110
+ renderer.render_tool_call(
111
+ tool_name: "file_reader",
112
+ formatted_call: "file_reader(path: 'test.rb')"
113
+ )
114
+ renderer.render_tool_result(result: "Success")
115
+
116
+ # Render status
117
+ renderer.render_status(
118
+ iteration: 5,
119
+ cost: 0.1234,
120
+ tasks_completed: 3,
121
+ tasks_total: 10
122
+ )
123
+ ```
124
+
125
+ ### OutputArea
126
+
127
+ Scrollable output buffer with automatic line wrapping.
128
+
129
+ ```ruby
130
+ output = Clacky::UI2::Components::OutputArea.new(height: 20)
131
+
132
+ output.append("Line 1")
133
+ output.append("Line 2\nLine 3")
134
+
135
+ output.scroll_up(5)
136
+ output.scroll_down(2)
137
+ output.scroll_to_top
138
+ output.scroll_to_bottom
139
+
140
+ output.at_bottom? # => true/false
141
+ output.scroll_percentage # => 0.0 to 100.0
142
+ ```
143
+
144
+ ### InputArea
145
+
146
+ Fixed input area with cursor support and history.
147
+
148
+ ```ruby
149
+ input = Clacky::UI2::Components::InputArea.new(height: 2)
150
+
151
+ input.insert_char("H")
152
+ input.backspace
153
+ input.cursor_left
154
+ input.cursor_right
155
+
156
+ value = input.submit # Returns and clears input
157
+ input.history_prev # Navigate history
158
+ ```
159
+
160
+ ### LayoutManager
161
+
162
+ Manages screen layout and coordinates rendering.
163
+
164
+ ```ruby
165
+ layout = Clacky::UI2::LayoutManager.new(
166
+ output_area: output,
167
+ input_area: input
168
+ )
169
+
170
+ layout.initialize_screen
171
+ layout.append_output("Hello")
172
+ layout.move_input_to_output
173
+ layout.scroll_output_up(5)
174
+ layout.cleanup_screen
175
+ ```
176
+
177
+ ## Keyboard Shortcuts
178
+
179
+ - **Enter** - Submit input
180
+ - **Ctrl+C** - Exit/Interrupt
181
+ - **Ctrl+L** - Clear output
182
+ - **Ctrl+U** - Clear input line
183
+ - **Up/Down** - Scroll output (when input empty) or navigate history
184
+ - **Left/Right** - Move cursor in input
185
+ - **Home/End** - Jump to start/end of input
186
+ - **Backspace** - Delete character before cursor
187
+ - **Delete** - Delete character at cursor
188
+
189
+ ## Layout Structure
190
+
191
+ ```
192
+ +----------------------------------------+
193
+ | Output Area (Scrollable) | <- Lines 0 to height-4
194
+ | [<<] Assistant: Hello... |
195
+ | [=>] Tool: file_reader |
196
+ | [<=] Result: ... |
197
+ | ... |
198
+ +----------------------------------------+ <- Separator
199
+ | [>>] Input: _ | <- Input line
200
+ +----------------------------------------+ <- Session bar
201
+ | Mode: confirm_safes | Tasks: 5 | $0.01 |
202
+ +----------------------------------------+
203
+ ```
204
+
205
+ ## Design Principles
206
+
207
+ 1. **Simplicity**: Agent directly calls UIController methods - no middleware
208
+ 2. **Dependency Injection**: Agent receives `ui:` parameter
209
+ 3. **Component-Based**: Reusable, testable UI components
210
+ 4. **Responsive**: Handles terminal resize and edge cases
211
+
212
+ ## License
213
+
214
+ MIT
@@ -0,0 +1,163 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module Clacky
6
+ module UI2
7
+ module Components
8
+ # BaseComponent provides common functionality for all UI components
9
+ class BaseComponent
10
+ def initialize
11
+ @pastel = Pastel.new
12
+ end
13
+
14
+ # Render component with given data
15
+ # @param data [Hash] Data to render
16
+ # @return [String] Rendered output
17
+ def render(data)
18
+ raise NotImplementedError, "Subclasses must implement render method"
19
+ end
20
+
21
+ # Class method to render without instantiating
22
+ # @param data [Hash] Data to render
23
+ # @return [String] Rendered output
24
+ def self.render(data)
25
+ new.render(data)
26
+ end
27
+
28
+ protected
29
+
30
+ # Get current theme from ThemeManager
31
+ # @return [Themes::BaseTheme] Current theme instance
32
+ def theme
33
+ UI2::ThemeManager.current_theme
34
+ end
35
+
36
+ # Format symbol with color from theme
37
+ # @param symbol_key [Symbol] Symbol key (e.g., :user, :assistant)
38
+ # @return [String] Colored symbol
39
+ def format_symbol(symbol_key)
40
+ theme.format_symbol(symbol_key)
41
+ end
42
+
43
+ # Format text with color from theme
44
+ # @param text [String] Text to format
45
+ # @param symbol_key [Symbol] Symbol key for color lookup
46
+ # @return [String] Colored text
47
+ def format_text(text, symbol_key)
48
+ theme.format_text(text, symbol_key)
49
+ end
50
+
51
+ # Truncate text to max length
52
+ # @param text [String] Text to truncate
53
+ # @param max_length [Integer] Maximum length
54
+ # @return [String] Truncated text
55
+ def truncate(text, max_length)
56
+ return "" if text.nil? || text.empty?
57
+
58
+ cleaned = text.strip.gsub(/\s+/, ' ')
59
+
60
+ if cleaned.length > max_length
61
+ cleaned[0...max_length] + "..."
62
+ else
63
+ cleaned
64
+ end
65
+ end
66
+
67
+ # Wrap text to specified width
68
+ # @param text [String] Text to wrap
69
+ # @param width [Integer] Maximum width
70
+ # @return [Array<String>] Array of wrapped lines
71
+ def wrap_text(text, width)
72
+ return [] if text.nil? || text.empty?
73
+
74
+ words = text.split(/\s+/)
75
+ lines = []
76
+ current_line = ""
77
+
78
+ words.each do |word|
79
+ if current_line.empty?
80
+ current_line = word
81
+ elsif (current_line.length + word.length + 1) <= width
82
+ current_line += " #{word}"
83
+ else
84
+ lines << current_line
85
+ current_line = word
86
+ end
87
+ end
88
+
89
+ lines << current_line unless current_line.empty?
90
+ lines
91
+ end
92
+
93
+ # Format timestamp
94
+ # @param time [Time] Time object
95
+ # @return [String] Formatted timestamp
96
+ def format_timestamp(time = Time.now)
97
+ time.strftime("%H:%M:%S")
98
+ end
99
+
100
+ # Create indented text
101
+ # @param text [String] Text to indent
102
+ # @param spaces [Integer] Number of spaces
103
+ # @return [String] Indented text
104
+ def indent(text, spaces = 2)
105
+ prefix = " " * spaces
106
+ text.split("\n").map { |line| "#{prefix}#{line}" }.join("\n")
107
+ end
108
+
109
+ # Format key-value pair
110
+ # @param key [String] Key name
111
+ # @param value [String] Value
112
+ # @return [String] Formatted key-value
113
+ def format_key_value(key, value)
114
+ "#{@pastel.cyan(key)}: #{@pastel.white(value)}"
115
+ end
116
+
117
+ # Create a separator line
118
+ # @param char [String] Character to use
119
+ # @param width [Integer] Width of separator
120
+ # @return [String] Separator line
121
+ def separator(char = "─", width = 80)
122
+ @pastel.dim(char * width)
123
+ end
124
+
125
+ # Format list item
126
+ # @param text [String] Item text
127
+ # @param bullet [String] Bullet character
128
+ # @return [String] Formatted list item
129
+ def format_list_item(text, bullet = "•")
130
+ "#{@pastel.dim(bullet)} #{@pastel.white(text)}"
131
+ end
132
+
133
+ # Format code block
134
+ # @param code [String] Code content
135
+ # @param language [String, nil] Language for syntax highlighting hint
136
+ # @return [String] Formatted code block
137
+ def format_code_block(code, language = nil)
138
+ header = language ? @pastel.dim("```#{language}") : @pastel.dim("```")
139
+ footer = @pastel.dim("```")
140
+ content = @pastel.cyan(code)
141
+
142
+ "#{header}\n#{content}\n#{footer}"
143
+ end
144
+
145
+ # Format progress bar
146
+ # @param current [Integer] Current value
147
+ # @param total [Integer] Total value
148
+ # @param width [Integer] Bar width
149
+ # @return [String] Progress bar
150
+ def format_progress_bar(current, total, width = 20)
151
+ return "" if total == 0
152
+
153
+ percentage = (current.to_f / total * 100).round(1)
154
+ filled = (current.to_f / total * width).round
155
+ empty = width - filled
156
+
157
+ bar = @pastel.green("█" * filled) + @pastel.dim("░" * empty)
158
+ "#{bar} #{percentage}%"
159
+ end
160
+ end
161
+ end
162
+ end
163
+ end
@@ -0,0 +1,89 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_component"
4
+
5
+ module Clacky
6
+ module UI2
7
+ module Components
8
+ # CommonComponent renders common UI elements (progress, success, error, warning)
9
+ class CommonComponent < BaseComponent
10
+ # Render thinking indicator
11
+ # @return [String] Thinking indicator
12
+ def render_thinking
13
+ symbol = format_symbol(:thinking)
14
+ text = format_text("Thinking...", :thinking)
15
+ "#{symbol} #{text}"
16
+ end
17
+
18
+ # Render progress indicator
19
+ # @param message [String] Progress message
20
+ # @return [String] Progress indicator
21
+ def render_progress(message)
22
+ symbol = format_symbol(:thinking)
23
+ text = format_text(message, :thinking)
24
+ "#{symbol} #{text}"
25
+ end
26
+
27
+ # Render success message
28
+ # @param message [String] Success message
29
+ # @return [String] Success message
30
+ def render_success(message)
31
+ symbol = format_symbol(:success)
32
+ text = format_text(message, :success)
33
+ "#{symbol} #{text}"
34
+ end
35
+
36
+ # Render error message
37
+ # @param message [String] Error message
38
+ # @return [String] Error message
39
+ def render_error(message)
40
+ symbol = format_symbol(:error)
41
+ text = format_text(message, :error)
42
+ "#{symbol} #{text}"
43
+ end
44
+
45
+ # Render warning message
46
+ # @param message [String] Warning message
47
+ # @return [String] Warning message
48
+ def render_warning(message)
49
+ symbol = format_symbol(:warning)
50
+ text = format_text(message, :warning)
51
+ "#{symbol} #{text}"
52
+ end
53
+
54
+ # Render task completion summary
55
+ # @param iterations [Integer] Number of iterations
56
+ # @param cost [Float] Cost in USD
57
+ # @param duration [Float] Duration in seconds
58
+ # @param cache_tokens [Integer] Cache read tokens
59
+ # @param cache_requests [Integer] Total cache requests count
60
+ # @param cache_hits [Integer] Cache hit requests count
61
+ # @return [String] Formatted completion summary
62
+ def render_task_complete(iterations:, cost:, duration: nil, cache_tokens: nil, cache_requests: nil, cache_hits: nil)
63
+ lines = []
64
+ lines << ""
65
+ lines << @pastel.dim("─" * 60)
66
+ lines << render_success("Task Complete")
67
+ lines << ""
68
+
69
+ # Display each stat on a separate line
70
+ lines << " Iterations: #{iterations}"
71
+ lines << " Cost: $#{cost.round(4)}"
72
+ lines << " Duration: #{duration.round(1)}s" if duration
73
+
74
+ # Display cache information if available
75
+ if cache_tokens && cache_tokens > 0
76
+ lines << " Cache Tokens: #{cache_tokens} tokens"
77
+ end
78
+
79
+ if cache_requests && cache_requests > 0
80
+ hit_rate = cache_hits > 0 ? ((cache_hits.to_f / cache_requests) * 100).round(1) : 0
81
+ lines << " Cache Requests: #{cache_requests} (#{cache_hits} hits, #{hit_rate}% hit rate)"
82
+ end
83
+
84
+ lines.join("\n")
85
+ end
86
+ end
87
+ end
88
+ end
89
+ end
@@ -0,0 +1,187 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../line_editor"
4
+
5
+ module Clacky
6
+ module UI2
7
+ module Components
8
+ # InlineInput provides inline input for confirmations and simple prompts
9
+ # Renders at the end of output area, not at fixed bottom position
10
+ class InlineInput
11
+ include LineEditor
12
+
13
+ attr_reader :prompt, :default_value
14
+
15
+ def initialize(prompt: "", default: nil)
16
+ initialize_line_editor
17
+ @prompt = prompt
18
+ @default_value = default
19
+ @active = false
20
+ @result_queue = nil
21
+ @paste_counter = 0
22
+ @paste_placeholders = {}
23
+ end
24
+
25
+ # Activate inline input and wait for user input
26
+ # @return [String] User input
27
+ def collect
28
+ @active = true
29
+ @result_queue = Queue.new
30
+ # Don't set default as initial text - start empty
31
+ @result_queue.pop
32
+ end
33
+
34
+ # Check if active
35
+ def active?
36
+ @active
37
+ end
38
+
39
+ # Handle keyboard input
40
+ # @param key [Symbol, String] Key input
41
+ # @return [Hash] Result with action
42
+ def handle_key(key)
43
+ return { action: nil } unless @active
44
+
45
+ case key
46
+ when Hash
47
+ if key[:type] == :rapid_input
48
+ # Handle multi-line paste with placeholder
49
+ pasted_text = key[:text]
50
+ pasted_lines = pasted_text.split(/\r\n|\r|\n/)
51
+
52
+ if pasted_lines.size > 1
53
+ # Multi-line paste - use placeholder
54
+ @paste_counter += 1
55
+ placeholder = "[##{@paste_counter} Paste Text]"
56
+ @paste_placeholders[placeholder] = pasted_text
57
+ insert_text(placeholder)
58
+ else
59
+ # Single line - insert directly
60
+ insert_text(pasted_text)
61
+ end
62
+ end
63
+ { action: :update }
64
+ when :enter
65
+ handle_enter
66
+ when :backspace
67
+ backspace
68
+ { action: :update }
69
+ when :delete
70
+ delete_char
71
+ { action: :update }
72
+ when :left_arrow, :ctrl_b
73
+ cursor_left
74
+ { action: :update }
75
+ when :right_arrow, :ctrl_f
76
+ cursor_right
77
+ { action: :update }
78
+ when :home, :ctrl_a
79
+ cursor_home
80
+ { action: :update }
81
+ when :end, :ctrl_e
82
+ cursor_end
83
+ { action: :update }
84
+ when :ctrl_k
85
+ kill_to_end
86
+ { action: :update }
87
+ when :ctrl_u
88
+ kill_to_start
89
+ { action: :update }
90
+ when :ctrl_w
91
+ kill_word
92
+ { action: :update }
93
+ when :shift_tab
94
+ handle_shift_tab
95
+ when :ctrl_c
96
+ handle_cancel
97
+ when :escape
98
+ handle_cancel
99
+ else
100
+ if key.is_a?(String) && key.length >= 1 && key.ord >= 32
101
+ insert_char(key)
102
+ { action: :update }
103
+ else
104
+ { action: nil }
105
+ end
106
+ end
107
+ end
108
+
109
+ # Render inline input with prompt and cursor
110
+ # @return [String] Rendered line (may wrap to multiple lines)
111
+ def render
112
+ line = render_line_with_cursor
113
+ full_text = "#{@prompt}#{line}"
114
+
115
+ # Calculate terminal width and check if wrapping is needed
116
+ width = TTY::Screen.width
117
+ visible_text = strip_ansi_codes(full_text)
118
+ display_width = calculate_display_width(visible_text)
119
+
120
+ # If no wrapping needed, return as is
121
+ return full_text if display_width <= width
122
+
123
+ # Otherwise, wrap the input (prompt on first line, continuation indented)
124
+ prompt_width = calculate_display_width(strip_ansi_codes(@prompt))
125
+ available_width = width - prompt_width
126
+
127
+ # For simplicity, just return full text and let terminal handle wrapping
128
+ # InlineInput is typically short, so natural wrapping should be fine
129
+ full_text
130
+ end
131
+
132
+ # Get cursor column position
133
+ # @return [Integer] Column position
134
+ def cursor_col
135
+ cursor_column(@prompt)
136
+ end
137
+
138
+ # Deactivate inline input
139
+ def deactivate
140
+ @active = false
141
+ @result_queue = nil
142
+ end
143
+
144
+ private
145
+
146
+ def handle_enter
147
+ result = expand_placeholders(current_line)
148
+ # If empty and has default, use default
149
+ result = @default_value.to_s if result.empty? && @default_value
150
+
151
+ queue = @result_queue
152
+ deactivate
153
+ queue&.push(result)
154
+
155
+ { action: :submit, result: result }
156
+ end
157
+
158
+ def expand_placeholders(text)
159
+ super(text, @paste_placeholders)
160
+ end
161
+
162
+ def handle_cancel
163
+ queue = @result_queue
164
+ deactivate
165
+ queue&.push(nil)
166
+
167
+ { action: :cancel }
168
+ end
169
+
170
+ def handle_shift_tab
171
+ # Auto-confirm as yes (or use default if it's true)
172
+ result = if @default_value == true || @default_value.to_s.downcase == "yes"
173
+ @default_value.to_s
174
+ else
175
+ "yes"
176
+ end
177
+
178
+ queue = @result_queue
179
+ deactivate
180
+ queue&.push(result)
181
+
182
+ { action: :toggle_mode }
183
+ end
184
+ end
185
+ end
186
+ end
187
+ end