openclacky 0.5.6 → 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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/docs/ui2-architecture.md +124 -0
  4. data/lib/clacky/agent.rb +245 -340
  5. data/lib/clacky/agent_config.rb +1 -7
  6. data/lib/clacky/cli.rb +156 -397
  7. data/lib/clacky/client.rb +68 -36
  8. data/lib/clacky/gitignore_parser.rb +26 -12
  9. data/lib/clacky/model_pricing.rb +6 -2
  10. data/lib/clacky/session_manager.rb +6 -2
  11. data/lib/clacky/tools/glob.rb +65 -9
  12. data/lib/clacky/tools/grep.rb +4 -120
  13. data/lib/clacky/tools/run_project.rb +5 -0
  14. data/lib/clacky/tools/safe_shell.rb +49 -13
  15. data/lib/clacky/tools/shell.rb +1 -49
  16. data/lib/clacky/tools/web_fetch.rb +2 -2
  17. data/lib/clacky/tools/web_search.rb +38 -26
  18. data/lib/clacky/ui2/README.md +214 -0
  19. data/lib/clacky/ui2/components/base_component.rb +163 -0
  20. data/lib/clacky/ui2/components/common_component.rb +89 -0
  21. data/lib/clacky/ui2/components/inline_input.rb +187 -0
  22. data/lib/clacky/ui2/components/input_area.rb +1029 -0
  23. data/lib/clacky/ui2/components/message_component.rb +76 -0
  24. data/lib/clacky/ui2/components/output_area.rb +112 -0
  25. data/lib/clacky/ui2/components/todo_area.rb +137 -0
  26. data/lib/clacky/ui2/components/tool_component.rb +106 -0
  27. data/lib/clacky/ui2/components/welcome_banner.rb +93 -0
  28. data/lib/clacky/ui2/layout_manager.rb +331 -0
  29. data/lib/clacky/ui2/line_editor.rb +201 -0
  30. data/lib/clacky/ui2/screen_buffer.rb +238 -0
  31. data/lib/clacky/ui2/theme_manager.rb +68 -0
  32. data/lib/clacky/ui2/themes/base_theme.rb +99 -0
  33. data/lib/clacky/ui2/themes/hacker_theme.rb +56 -0
  34. data/lib/clacky/ui2/themes/minimal_theme.rb +50 -0
  35. data/lib/clacky/ui2/ui_controller.rb +720 -0
  36. data/lib/clacky/ui2/view_renderer.rb +160 -0
  37. data/lib/clacky/ui2.rb +37 -0
  38. data/lib/clacky/utils/file_ignore_helper.rb +126 -0
  39. data/lib/clacky/version.rb +1 -1
  40. data/lib/clacky.rb +1 -6
  41. metadata +38 -6
  42. data/lib/clacky/ui/banner.rb +0 -155
  43. data/lib/clacky/ui/enhanced_prompt.rb +0 -786
  44. data/lib/clacky/ui/formatter.rb +0 -209
  45. data/lib/clacky/ui/statusbar.rb +0 -96
@@ -95,7 +95,7 @@ module Clacky
95
95
  if elapsed > soft_timeout && !soft_timeout_triggered
96
96
  soft_timeout_triggered = true
97
97
 
98
- # L1:
98
+ # L1: Check for interaction patterns
99
99
  interaction = detect_interaction(stdout_buffer.string)
100
100
  if interaction
101
101
  Process.kill('TERM', wait_thr.pid) rescue nil
@@ -107,24 +107,6 @@ module Clacky
107
107
  max_output_lines
108
108
  )
109
109
  end
110
-
111
- # L2:
112
- last_size = stdout_buffer.size
113
- stdin.puts("\n") rescue nil
114
- sleep 2
115
-
116
- if stdout_buffer.size > last_size
117
- next
118
- else
119
- Process.kill('TERM', wait_thr.pid) rescue nil
120
- return format_stuck_result(
121
- command,
122
- stdout_buffer.string,
123
- stderr_buffer.string,
124
- elapsed,
125
- max_output_lines
126
- )
127
- end
128
110
  end
129
111
 
130
112
  break unless wait_thr.alive?
@@ -255,36 +237,6 @@ module Clacky
255
237
  MSG
256
238
  end
257
239
 
258
- def format_stuck_result(command, stdout, stderr, elapsed, max_output_lines)
259
- {
260
- command: command,
261
- stdout: truncate_output(stdout, max_output_lines),
262
- stderr: truncate_output(stderr, max_output_lines),
263
- exit_code: -3,
264
- success: false,
265
- state: 'STUCK',
266
- elapsed: elapsed,
267
- message: format_stuck_message(truncate_output(stdout, max_output_lines), elapsed),
268
- output_truncated: output_truncated?(stdout, stderr, max_output_lines)
269
- }
270
- end
271
-
272
- def format_stuck_message(output, elapsed)
273
- <<~MSG
274
- #{output}
275
-
276
- #{'=' * 60}
277
- [Terminal State: STUCK]
278
- #{'=' * 60}
279
-
280
- The terminal is not responding after #{elapsed.round(1)}s.
281
-
282
- Suggested actions:
283
- • Try interrupting with Ctrl+C
284
- • Check if command is frozen
285
- MSG
286
- end
287
-
288
240
  def format_timeout_result(command, stdout, stderr, elapsed, type, timeout, max_output_lines)
289
241
  {
290
242
  command: command,
@@ -40,8 +40,8 @@ module Clacky
40
40
  # Fetch the web page
41
41
  response = fetch_url(uri)
42
42
 
43
- # Extract content
44
- content = response.body
43
+ # Extract content and force UTF-8 encoding at the source
44
+ content = response.body.force_encoding('UTF-8').scrub('?')
45
45
  content_type = response["content-type"] || ""
46
46
 
47
47
  # Parse HTML if it's an HTML page
@@ -48,14 +48,14 @@ module Clacky
48
48
  end
49
49
  end
50
50
 
51
- def search_duckduckgo(query, max_results)
51
+ private def search_duckduckgo(query, max_results)
52
52
  # DuckDuckGo HTML search endpoint
53
53
  encoded_query = CGI.escape(query)
54
54
  url = URI("https://html.duckduckgo.com/html/?q=#{encoded_query}")
55
55
 
56
56
  # Make request with user agent
57
57
  request = Net::HTTP::Get.new(url)
58
- request["User-Agent"] = "Mozilla/5.0 (compatible; Clacky/1.0)"
58
+ request["User-Agent"] = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"
59
59
 
60
60
  response = Net::HTTP.start(url.hostname, url.port, use_ssl: true, read_timeout: 10) do |http|
61
61
  http.request(request)
@@ -78,45 +78,57 @@ module Clacky
78
78
  ]
79
79
  end
80
80
 
81
- def parse_duckduckgo_html(html, max_results)
81
+ private def parse_duckduckgo_html(html, max_results)
82
82
  results = []
83
83
 
84
- # Simple regex-based parsing (not perfect but works for basic cases)
85
- # Look for result blocks in DuckDuckGo HTML
86
- html.scan(%r{<div class="result__body">.*?</div>}m).each do |block|
84
+ # Ensure HTML is UTF-8 encoded
85
+ html = html.force_encoding('UTF-8') unless html.encoding == Encoding::UTF_8
86
+
87
+ # Extract all result links and snippets
88
+ # Pattern: <a class="result__a" href="//duckduckgo.com/l/?uddg=ENCODED_URL...">TITLE</a>
89
+ links = html.scan(%r{<a[^>]*class="result__a"[^>]*href="//duckduckgo\.com/l/\?uddg=([^"&]+)[^"]*"[^>]*>(.*?)</a>}m)
90
+
91
+ # Pattern: <a class="result__snippet">SNIPPET</a>
92
+ snippets = html.scan(%r{<a[^>]*class="result__snippet"[^>]*>(.*?)</a>}m)
93
+
94
+ # Combine links and snippets
95
+ links.each_with_index do |link_data, index|
87
96
  break if results.length >= max_results
88
97
 
89
- # Extract title and URL
90
- if block =~ %r{<a.*?href="//duckduckgo\.com/l/\?uddg=([^"&]+).*?".*?>(.*?)</a>}m
91
- url = CGI.unescape($1)
92
- title = $2.gsub(/<[^>]+>/, "").strip
93
-
94
- # Extract snippet
95
- snippet = ""
96
- if block =~ %r{<a class="result__snippet".*?>(.*?)</a>}m
97
- snippet = $1.gsub(/<[^>]+>/, "").strip
98
- end
99
-
100
- results << {
101
- title: title,
102
- url: url,
103
- snippet: snippet
104
- }
98
+ url = CGI.unescape(link_data[0]).force_encoding('UTF-8')
99
+ title = link_data[1].gsub(/<[^>]+>/, "").strip
100
+ title = CGI.unescapeHTML(title) if title.include?("&")
101
+
102
+ snippet = ""
103
+ if snippets[index]
104
+ snippet = snippets[index][0].gsub(/<[^>]+>/, "").strip
105
+ snippet = CGI.unescapeHTML(snippet) if snippet.include?("&")
105
106
  end
107
+
108
+ results << {
109
+ title: title,
110
+ url: url,
111
+ snippet: snippet
112
+ }
106
113
  end
107
114
 
108
115
  # If parsing failed, provide a fallback
109
116
  if results.empty?
110
117
  results << {
111
118
  title: "Web search results",
112
- url: "https://duckduckgo.com/?q=#{CGI.escape(query)}",
113
- snippet: "Could not parse search results. Visit the URL to see results."
119
+ url: "https://duckduckgo.com/",
120
+ snippet: "Could not parse search results. Please try again."
114
121
  }
115
122
  end
116
123
 
117
124
  results
118
- rescue StandardError
119
- []
125
+ rescue StandardError => e
126
+ # Return fallback on error
127
+ [{
128
+ title: "Web search error",
129
+ url: "https://duckduckgo.com/",
130
+ snippet: "Error parsing results: #{e.message}"
131
+ }]
120
132
  end
121
133
 
122
134
  def format_call(args)
@@ -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