openclacky 0.6.0 → 0.6.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.
Files changed (38) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +54 -0
  3. data/README.md +39 -88
  4. data/homebrew/README.md +96 -0
  5. data/homebrew/openclacky.rb +24 -0
  6. data/lib/clacky/agent.rb +139 -67
  7. data/lib/clacky/cli.rb +105 -6
  8. data/lib/clacky/tools/file_reader.rb +135 -2
  9. data/lib/clacky/tools/glob.rb +2 -2
  10. data/lib/clacky/tools/grep.rb +2 -2
  11. data/lib/clacky/tools/run_project.rb +5 -5
  12. data/lib/clacky/tools/safe_shell.rb +140 -17
  13. data/lib/clacky/tools/shell.rb +69 -2
  14. data/lib/clacky/tools/todo_manager.rb +50 -3
  15. data/lib/clacky/tools/trash_manager.rb +1 -1
  16. data/lib/clacky/tools/web_fetch.rb +2 -2
  17. data/lib/clacky/tools/web_search.rb +2 -2
  18. data/lib/clacky/ui2/components/common_component.rb +14 -5
  19. data/lib/clacky/ui2/components/input_area.rb +300 -89
  20. data/lib/clacky/ui2/components/message_component.rb +7 -3
  21. data/lib/clacky/ui2/components/todo_area.rb +38 -45
  22. data/lib/clacky/ui2/components/welcome_banner.rb +10 -0
  23. data/lib/clacky/ui2/layout_manager.rb +180 -50
  24. data/lib/clacky/ui2/markdown_renderer.rb +80 -0
  25. data/lib/clacky/ui2/screen_buffer.rb +26 -7
  26. data/lib/clacky/ui2/themes/base_theme.rb +32 -46
  27. data/lib/clacky/ui2/themes/hacker_theme.rb +4 -2
  28. data/lib/clacky/ui2/themes/minimal_theme.rb +4 -2
  29. data/lib/clacky/ui2/ui_controller.rb +150 -32
  30. data/lib/clacky/ui2/view_renderer.rb +21 -4
  31. data/lib/clacky/ui2.rb +0 -1
  32. data/lib/clacky/utils/arguments_parser.rb +7 -2
  33. data/lib/clacky/utils/file_processor.rb +201 -0
  34. data/lib/clacky/version.rb +1 -1
  35. data/scripts/install.sh +249 -0
  36. data/scripts/uninstall.sh +146 -0
  37. metadata +21 -2
  38. data/lib/clacky/ui2/components/output_area.rb +0 -112
@@ -5,71 +5,43 @@ require "pastel"
5
5
  module Clacky
6
6
  module UI2
7
7
  module Themes
8
- # BaseTheme defines the interface for all themes
9
- # Subclasses should override SYMBOLS and color methods
8
+ # BaseTheme defines the abstract interface for all themes
9
+ # Subclasses MUST define SYMBOLS and COLORS constants
10
10
  class BaseTheme
11
- SYMBOLS = {
12
- user: "[>>]",
13
- assistant: "[<<]",
14
- tool_call: "[=>]",
15
- tool_result: "[<=]",
16
- tool_denied: "[!!]",
17
- tool_planned: "[??]",
18
- tool_error: "[XX]",
19
- thinking: "[..]",
20
- success: "[OK]",
21
- error: "[ER]",
22
- warning: "[!!]",
23
- info: "[--]",
24
- task: "[##]",
25
- progress: "[>>]",
26
- file: "[F]",
27
- command: "[C]",
28
- cached: "[*]"
29
- }.freeze
30
-
31
- # Color schemes for different elements
32
- # Each returns [symbol_color, text_color]
33
- COLORS = {
34
- user: [:bright_blue, :blue],
35
- assistant: [:bright_green, :white],
36
- tool_call: [:bright_cyan, :cyan],
37
- tool_result: [:cyan, :white],
38
- tool_denied: [:bright_yellow, :yellow],
39
- tool_planned: [:bright_blue, :blue],
40
- tool_error: [:bright_red, :red],
41
- thinking: [:dim, :dim],
42
- success: [:bright_green, :green],
43
- error: [:bright_red, :red],
44
- warning: [:bright_yellow, :yellow],
45
- info: [:bright_white, :white],
46
- task: [:bright_yellow, :white],
47
- progress: [:bright_cyan, :cyan],
48
- file: [:cyan, :white],
49
- command: [:cyan, :white],
50
- cached: [:cyan, :cyan]
51
- }.freeze
52
-
53
11
  def initialize
54
12
  @pastel = Pastel.new
13
+ validate_theme_definition!
55
14
  end
56
15
 
16
+ # Get all symbols defined by this theme
17
+ # @return [Hash] Symbol definitions
57
18
  def symbols
58
19
  self.class::SYMBOLS
59
20
  end
60
21
 
22
+ # Get all colors defined by this theme
23
+ # @return [Hash] Color definitions
61
24
  def colors
62
25
  self.class::COLORS
63
26
  end
64
27
 
28
+ # Get symbol for a specific key
29
+ # @param key [Symbol] Symbol key
30
+ # @return [String] Symbol string
65
31
  def symbol(key)
66
32
  symbols[key] || "[??]"
67
33
  end
68
34
 
35
+ # Get symbol color for a specific key
36
+ # @param key [Symbol] Color key
37
+ # @return [Symbol] Pastel color method name
69
38
  def symbol_color(key)
70
39
  colors.dig(key, 0) || :white
71
40
  end
72
41
 
42
+ # Get text color for a specific key
43
+ # @param key [Symbol] Color key
44
+ # @return [Symbol] Pastel color method name
73
45
  def text_color(key)
74
46
  colors.dig(key, 1) || :white
75
47
  end
@@ -89,9 +61,23 @@ module Clacky
89
61
  @pastel.public_send(text_color(key), text)
90
62
  end
91
63
 
92
- # Theme name for display
64
+ # Theme name for display (subclasses should override)
65
+ # @return [String] Theme name
93
66
  def name
94
- "base"
67
+ raise NotImplementedError, "Subclass must implement #name method"
68
+ end
69
+
70
+ private
71
+
72
+ # Validate that subclass has defined required constants
73
+ def validate_theme_definition!
74
+ unless self.class.const_defined?(:SYMBOLS)
75
+ raise NotImplementedError, "Theme #{self.class.name} must define SYMBOLS constant"
76
+ end
77
+
78
+ unless self.class.const_defined?(:COLORS)
79
+ raise NotImplementedError, "Theme #{self.class.name} must define COLORS constant"
80
+ end
95
81
  end
96
82
  end
97
83
  end
@@ -16,6 +16,7 @@ module Clacky
16
16
  tool_planned: "[??]",
17
17
  tool_error: "[XX]",
18
18
  thinking: "[..]",
19
+ working: "[..]",
19
20
  success: "[OK]",
20
21
  error: "[ER]",
21
22
  warning: "[!!]",
@@ -28,14 +29,15 @@ module Clacky
28
29
  }.freeze
29
30
 
30
31
  COLORS = {
31
- user: [:bright_blue, :blue],
32
+ user: [:white, :white],
32
33
  assistant: [:bright_green, :white],
33
34
  tool_call: [:bright_cyan, :cyan],
34
35
  tool_result: [:cyan, :white],
35
36
  tool_denied: [:bright_yellow, :yellow],
36
- tool_planned: [:bright_blue, :blue],
37
+ tool_planned: [:bright_cyan, :cyan],
37
38
  tool_error: [:bright_red, :red],
38
39
  thinking: [:dim, :dim],
40
+ working: [:bright_yellow, :yellow],
39
41
  success: [:bright_green, :green],
40
42
  error: [:bright_red, :red],
41
43
  warning: [:bright_yellow, :yellow],
@@ -16,6 +16,7 @@ module Clacky
16
16
  tool_planned: "?",
17
17
  tool_error: "x",
18
18
  thinking: ".",
19
+ working: ".",
19
20
  success: "+",
20
21
  error: "x",
21
22
  warning: "!",
@@ -25,14 +26,15 @@ module Clacky
25
26
  }.freeze
26
27
 
27
28
  COLORS = {
28
- user: [:blue, :blue],
29
+ user: [:white, :white],
29
30
  assistant: [:green, :white],
30
31
  tool_call: [:cyan, :cyan],
31
32
  tool_result: [:white, :white],
32
33
  tool_denied: [:yellow, :yellow],
33
- tool_planned: [:blue, :blue],
34
+ tool_planned: [:cyan, :cyan],
34
35
  tool_error: [:red, :red],
35
36
  thinking: [:dim, :dim],
37
+ working: [:bright_yellow, :yellow],
36
38
  success: [:green, :green],
37
39
  error: [:red, :red],
38
40
  warning: [:yellow, :yellow],
@@ -2,7 +2,6 @@
2
2
 
3
3
  require_relative "layout_manager"
4
4
  require_relative "view_renderer"
5
- require_relative "components/output_area"
6
5
  require_relative "components/input_area"
7
6
  require_relative "components/todo_area"
8
7
  require_relative "components/welcome_banner"
@@ -31,13 +30,11 @@ module Clacky
31
30
  }
32
31
 
33
32
  # Initialize layout components
34
- @output_area = Components::OutputArea.new(height: 20) # Will be recalculated
35
33
  @input_area = Components::InputArea.new
36
34
  @todo_area = Components::TodoArea.new
37
35
  @welcome_banner = Components::WelcomeBanner.new
38
36
  @inline_input = nil # Created when needed
39
37
  @layout = LayoutManager.new(
40
- output_area: @output_area,
41
38
  input_area: @input_area,
42
39
  todo_area: @todo_area
43
40
  )
@@ -59,7 +56,8 @@ module Clacky
59
56
  end
60
57
 
61
58
  # Initialize screen and show banner (separate from input loop)
62
- def initialize_and_show_banner
59
+ # @param recent_user_messages [Array<String>, nil] Recent user messages when loading session
60
+ def initialize_and_show_banner(recent_user_messages: nil)
63
61
  @running = true
64
62
 
65
63
  # Set session bar data before initializing screen
@@ -73,8 +71,12 @@ module Clacky
73
71
 
74
72
  @layout.initialize_screen
75
73
 
76
- # Display welcome banner
77
- display_welcome_banner
74
+ # Display welcome banner or session history
75
+ if recent_user_messages && !recent_user_messages.empty?
76
+ display_session_history(recent_user_messages)
77
+ else
78
+ display_welcome_banner
79
+ end
78
80
  end
79
81
 
80
82
  # Start input loop (separate from initialization)
@@ -206,19 +208,21 @@ module Clacky
206
208
  # - cost: cost for this iteration
207
209
  def show_token_usage(token_data)
208
210
  theme = ThemeManager.current_theme
211
+ pastel = Pastel.new
209
212
 
210
213
  token_info = []
211
214
 
212
- # Delta tokens with color coding
215
+ # Delta tokens with color coding (green/yellow/red + dim)
213
216
  delta_tokens = token_data[:delta_tokens]
214
217
  delta_str = "+#{delta_tokens}"
215
- colored_delta = if delta_tokens > 10000
216
- theme.format_text(delta_str, :error)
218
+ color_style = if delta_tokens > 10000
219
+ :red
217
220
  elsif delta_tokens > 5000
218
- theme.format_text(delta_str, :warning)
221
+ :yellow
219
222
  else
220
- theme.format_text(delta_str, :success)
223
+ :green
221
224
  end
225
+ colored_delta = pastel.decorate(delta_str, color_style, :dim)
222
226
  token_info << colored_delta
223
227
 
224
228
  # Cache status indicator (using theme)
@@ -226,31 +230,44 @@ module Clacky
226
230
  cache_read = token_data[:cache_read]
227
231
  cache_used = cache_read > 0 || cache_write > 0
228
232
  if cache_used
229
- token_info << theme.format_symbol(:cached)
233
+ token_info << pastel.dim(theme.symbol(:cached))
230
234
  end
231
235
 
232
236
  # Input tokens (with cache breakdown if available)
233
237
  prompt_tokens = token_data[:prompt_tokens]
234
238
  if cache_write > 0 || cache_read > 0
235
239
  input_detail = "#{prompt_tokens} (cache: #{cache_read} read, #{cache_write} write)"
236
- token_info << "Input: #{input_detail}"
240
+ token_info << pastel.dim("Input: #{input_detail}")
237
241
  else
238
- token_info << "Input: #{prompt_tokens}"
242
+ token_info << pastel.dim("Input: #{prompt_tokens}")
239
243
  end
240
244
 
241
245
  # Output tokens
242
- token_info << "Output: #{token_data[:completion_tokens]}"
246
+ token_info << pastel.dim("Output: #{token_data[:completion_tokens]}")
243
247
 
244
248
  # Total
245
- token_info << "Total: #{token_data[:total_tokens]}"
249
+ token_info << pastel.dim("Total: #{token_data[:total_tokens]}")
246
250
 
247
- # Cost for this iteration
251
+ # Cost for this iteration with color coding (red/yellow for high cost, dim for normal)
248
252
  if token_data[:cost]
249
- token_info << "Cost: $#{token_data[:cost].round(6)}"
253
+ cost = token_data[:cost]
254
+ cost_value = "$#{cost.round(6)}"
255
+ if cost >= 0.1
256
+ # High cost - red warning
257
+ colored_cost = pastel.decorate(cost_value, :red, :dim)
258
+ token_info << pastel.dim("Cost: ") + colored_cost
259
+ elsif cost >= 0.05
260
+ # Medium cost - yellow warning
261
+ colored_cost = pastel.decorate(cost_value, :yellow, :dim)
262
+ token_info << pastel.dim("Cost: ") + colored_cost
263
+ else
264
+ # Low cost - normal gray
265
+ token_info << pastel.dim("Cost: #{cost_value}")
266
+ end
250
267
  end
251
268
 
252
- # Display through output system
253
- token_display = theme.format_text(" [Tokens] #{token_info.join(' | ')}", :thinking)
269
+ # Display through output system (already all dimmed, just add prefix)
270
+ token_display = pastel.dim(" [Tokens] ") + token_info.join(pastel.dim(' | '))
254
271
  append_output(token_display)
255
272
  end
256
273
 
@@ -308,10 +325,29 @@ module Clacky
308
325
  # Show assistant message
309
326
  # @param content [String] Message content
310
327
  def show_assistant_message(content)
311
- output = @renderer.render_assistant_message(content)
328
+ # Filter out thinking tags from models like MiniMax M2.1 that use <think>...</think>
329
+ filtered_content = filter_thinking_tags(content)
330
+ return if filtered_content.nil? || filtered_content.strip.empty?
331
+
332
+ output = @renderer.render_assistant_message(filtered_content)
312
333
  append_output(output)
313
334
  end
314
335
 
336
+ # Filter out thinking tags from content
337
+ # Some models (e.g., MiniMax M2.1) wrap their reasoning in <think>...</think> tags
338
+ # @param content [String] Raw content from model
339
+ # @return [String] Content with thinking tags removed
340
+ def filter_thinking_tags(content)
341
+ return content if content.nil?
342
+
343
+ # Remove <think>...</think> blocks (multiline, case-insensitive)
344
+ # Also handles variations like <thinking>...</thinking>
345
+ filtered = content.gsub(%r{<think(?:ing)?>\s*.*?\s*</think(?:ing)?>}mi, '')
346
+
347
+ # Clean up extra whitespace left behind
348
+ filtered.gsub(/\n{3,}/, "\n\n").strip
349
+ end
350
+
315
351
  # Show tool call
316
352
  # @param name [String] Tool name
317
353
  # @param args [String, Hash] Tool arguments (JSON string or Hash)
@@ -344,6 +380,10 @@ module Clacky
344
380
  def show_complete(iterations:, cost:, duration: nil, cache_stats: nil)
345
381
  # Update status back to 'idle' when task is complete
346
382
  update_sessionbar(status: 'idle')
383
+
384
+ # Clear user tip when agent stops working
385
+ @input_area.clear_user_tip
386
+ @layout.render_input
347
387
 
348
388
  # Only show completion message for complex tasks (>5 iterations)
349
389
  return if iterations <= 5
@@ -351,7 +391,7 @@ module Clacky
351
391
  cache_tokens = cache_stats&.dig(:cache_read_input_tokens)
352
392
  cache_requests = cache_stats&.dig(:total_requests)
353
393
  cache_hits = cache_stats&.dig(:cache_hit_requests)
354
-
394
+
355
395
  output = @renderer.render_task_complete(
356
396
  iterations: iterations,
357
397
  cost: cost,
@@ -365,7 +405,8 @@ module Clacky
365
405
 
366
406
  # Show progress indicator with dynamic elapsed time
367
407
  # @param message [String] Progress message (optional, will use random thinking verb if nil)
368
- def show_progress(message = nil)
408
+ # @param prefix_newline [Boolean] Whether to add a blank line before progress (default: true)
409
+ def show_progress(message = nil, prefix_newline: true)
369
410
  # Stop any existing progress thread
370
411
  stop_progress_thread
371
412
 
@@ -375,8 +416,9 @@ module Clacky
375
416
  @progress_message = message || Clacky::THINKING_VERBS.sample
376
417
  @progress_start_time = Time.now
377
418
 
378
- # Show initial progress
379
- output = @renderer.render_progress("#{@progress_message}… (ctrl+c to interrupt)")
419
+ # Show initial progress (yellow, active)
420
+ append_output("") if prefix_newline
421
+ output = @renderer.render_working("#{@progress_message}… (ctrl+c to interrupt)")
380
422
  append_output(output)
381
423
 
382
424
  # Start background thread to update elapsed time
@@ -386,7 +428,7 @@ module Clacky
386
428
  next unless @progress_start_time
387
429
 
388
430
  elapsed = (Time.now - @progress_start_time).to_i
389
- update_progress_line(@renderer.render_progress("#{@progress_message}… (ctrl+c to interrupt · #{elapsed}s)"))
431
+ update_progress_line(@renderer.render_working("#{@progress_message}… (ctrl+c to interrupt · #{elapsed}s)"))
390
432
  end
391
433
  rescue => e
392
434
  # Silently handle thread errors
@@ -395,8 +437,19 @@ module Clacky
395
437
 
396
438
  # Clear progress indicator
397
439
  def clear_progress
440
+ # Calculate elapsed time before stopping
441
+ elapsed_time = @progress_start_time ? (Time.now - @progress_start_time).to_i : 0
442
+
443
+ # Stop the progress thread
398
444
  stop_progress_thread
399
- clear_progress_line
445
+
446
+ # Update the final progress line to gray (stopped state)
447
+ if @progress_message && elapsed_time > 0
448
+ final_output = @renderer.render_progress("#{@progress_message}… (#{elapsed_time}s)")
449
+ update_progress_line(final_output)
450
+ else
451
+ clear_progress_line
452
+ end
400
453
  end
401
454
 
402
455
  # Stop the progress update thread
@@ -410,8 +463,9 @@ module Clacky
410
463
 
411
464
  # Show info message
412
465
  # @param message [String] Info message
413
- def show_info(message)
414
- output = @renderer.render_system_message(message)
466
+ # @param prefix_newline [Boolean] Whether to add newline before message (default: true)
467
+ def show_info(message, prefix_newline: true)
468
+ output = @renderer.render_system_message(message, prefix_newline: prefix_newline)
415
469
  append_output(output)
416
470
  end
417
471
 
@@ -432,11 +486,17 @@ module Clacky
432
486
  # Set workspace status to idle (called when agent stops working)
433
487
  def set_idle_status
434
488
  update_sessionbar(status: 'idle')
489
+ # Clear user tip when agent stops working
490
+ @input_area.clear_user_tip
491
+ @layout.render_input
435
492
  end
436
493
 
437
494
  # Set workspace status to working (called when agent starts working)
438
495
  def set_working_status
439
496
  update_sessionbar(status: 'working')
497
+ # Show a random user tip with 40% probability when agent starts working
498
+ @input_area.show_user_tip(probability: 0.4)
499
+ @layout.render_input
440
500
  end
441
501
 
442
502
  # Show help text
@@ -484,7 +544,7 @@ module Clacky
484
544
 
485
545
  # Create InlineInput with styled prompt
486
546
  inline_input = Components::InlineInput.new(
487
- prompt: theme.format_text(" (y/n, or provide feedback): ", :thinking),
547
+ prompt: " Press Enter to approve, 'n' to reject, or provide feedback: ",
488
548
  default: nil
489
549
  )
490
550
  @inline_input = inline_input
@@ -535,9 +595,38 @@ module Clacky
535
595
 
536
596
  diff = Diffy::Diff.new(old_content, new_content, context: 3)
537
597
  all_lines = diff.to_s(:color).lines
538
- display_lines = all_lines.first(max_lines)
598
+ plain_lines = diff.to_s.lines
599
+
600
+ # Add line numbers to diff output
601
+ old_line_num = 0
602
+ new_line_num = 0
603
+
604
+ numbered_lines = all_lines.each_with_index.map do |line, index|
605
+ # Use plain text to detect line type (remove ANSI codes)
606
+ plain_line = plain_lines[index]&.chomp || line.gsub(/\e\[[0-9;]*m/, '').chomp
607
+
608
+ # Remove trailing newline from colored line to avoid double newlines
609
+ colored_line = line.chomp
610
+
611
+ # Determine line type and number (use single line number for simplicity)
612
+ if plain_line.start_with?('+') || plain_line.start_with?('-') || plain_line.start_with?(' ')
613
+ new_line_num += 1
614
+ sprintf("%4d | %s", new_line_num, colored_line)
615
+ elsif plain_line.start_with?('@@')
616
+ # Diff header: extract line numbers from @@ -old_start,old_count +new_start,new_count @@
617
+ if plain_line =~ /@@ -(\d+)(?:,\d+)? (\d+)(?:,\d+)? @@/
618
+ new_line_num = $2.to_i - 1
619
+ end
620
+ sprintf("%4s | %s", "", colored_line)
621
+ else
622
+ # Other lines (headers, etc.)
623
+ sprintf("%4s | %s", "", colored_line)
624
+ end
625
+ end
626
+
627
+ display_lines = numbered_lines.first(max_lines)
628
+ display_lines.each { |line| append_output(line) }
539
629
 
540
- display_lines.each { |line| append_output(line.chomp) }
541
630
  if all_lines.size > max_lines
542
631
  append_output("\n... (#{all_lines.size - max_lines} more lines, diff truncated)")
543
632
  end
@@ -599,6 +688,35 @@ module Clacky
599
688
  append_output(content)
600
689
  end
601
690
 
691
+ # Display recent user messages when loading session
692
+ # @param user_messages [Array<String>] Array of recent user message texts
693
+ def display_session_history(user_messages)
694
+ theme = ThemeManager.current_theme
695
+
696
+ # Show logo banner only
697
+ append_output(@welcome_banner.render_logo)
698
+
699
+ # Show simple header
700
+ append_output(theme.format_text("Recent conversation:", :info))
701
+
702
+ # Display each user message with numbering
703
+ user_messages.each_with_index do |msg, index|
704
+ # Truncate long messages
705
+ display_msg = if msg.length > 140
706
+ "#{msg[0..137]}..."
707
+ else
708
+ msg
709
+ end
710
+
711
+ # Show with number and indentation
712
+ append_output(" #{index + 1}. #{display_msg}")
713
+ end
714
+
715
+ # Bottom spacing and continuation prompt
716
+ append_output("")
717
+ append_output(theme.format_text("Session restored. Feel free to continue with your next task.", :success))
718
+ end
719
+
602
720
  # Main input loop
603
721
  def input_loop
604
722
  @layout.screen.enable_raw_mode
@@ -3,6 +3,7 @@
3
3
  require_relative "components/message_component"
4
4
  require_relative "components/tool_component"
5
5
  require_relative "components/common_component"
6
+ require_relative "markdown_renderer"
6
7
 
7
8
  module Clacky
8
9
  module UI2
@@ -31,9 +32,16 @@ module Clacky
31
32
  # @param timestamp [Time, nil] Optional timestamp
32
33
  # @return [String] Rendered message
33
34
  def render_assistant_message(content, timestamp: nil)
35
+ # Render markdown if content contains markdown syntax
36
+ rendered_content = if MarkdownRenderer.markdown?(content)
37
+ MarkdownRenderer.render(content)
38
+ else
39
+ content
40
+ end
41
+
34
42
  @message_component.render(
35
43
  role: "assistant",
36
- content: content,
44
+ content: rendered_content,
37
45
  timestamp: timestamp
38
46
  )
39
47
  end
@@ -41,12 +49,14 @@ module Clacky
41
49
  # Render a system message
42
50
  # @param content [String] Message content
43
51
  # @param timestamp [Time, nil] Optional timestamp
52
+ # @param prefix_newline [Boolean] Whether to add newline before message
44
53
  # @return [String] Rendered message
45
- def render_system_message(content, timestamp: nil)
54
+ def render_system_message(content, timestamp: nil, prefix_newline: true)
46
55
  @message_component.render(
47
56
  role: "system",
48
57
  content: content,
49
- timestamp: timestamp
58
+ timestamp: timestamp,
59
+ prefix_newline: prefix_newline
50
60
  )
51
61
  end
52
62
 
@@ -108,13 +118,20 @@ module Clacky
108
118
  @common_component.render_thinking
109
119
  end
110
120
 
111
- # Render progress message
121
+ # Render progress message (stopped state, gray)
112
122
  # @param message [String] Progress message
113
123
  # @return [String] Progress indicator
114
124
  def render_progress(message)
115
125
  @common_component.render_progress(message)
116
126
  end
117
127
 
128
+ # Render working message (active state, yellow)
129
+ # @param message [String] Progress message
130
+ # @return [String] Working indicator
131
+ def render_working(message)
132
+ @common_component.render_working(message)
133
+ end
134
+
118
135
  # Render success message
119
136
  # @param message [String] Success message
120
137
  # @return [String] Success message
data/lib/clacky/ui2.rb CHANGED
@@ -10,7 +10,6 @@ require_relative "ui2/view_renderer"
10
10
  require_relative "ui2/ui_controller"
11
11
 
12
12
  require_relative "ui2/components/base_component"
13
- require_relative "ui2/components/output_area"
14
13
  require_relative "ui2/components/input_area"
15
14
  require_relative "ui2/components/message_component"
16
15
  require_relative "ui2/components/tool_component"
@@ -44,10 +44,11 @@ module Clacky
44
44
  result
45
45
  end
46
46
 
47
- # Validate required parameters
47
+ # Validate required parameters and filter unknown parameters
48
48
  def self.validate_required_params(call, args, tool_registry)
49
49
  tool = tool_registry.get(call[:name])
50
50
  required = tool.parameters&.dig(:required) || []
51
+ properties = tool.parameters&.dig(:properties) || {}
51
52
 
52
53
  missing = required.reject { |param|
53
54
  args.key?(param.to_sym) || args.key?(param.to_s)
@@ -57,7 +58,11 @@ module Clacky
57
58
  raise MissingRequiredParamsError.new(call[:name], missing, args.keys)
58
59
  end
59
60
 
60
- args
61
+ # Filter out unknown parameters to prevent errors when LLM sends extra arguments
62
+ known_params = properties.keys.map(&:to_sym) + properties.keys.map(&:to_s)
63
+ filtered_args = args.select { |key, _| known_params.include?(key) }
64
+
65
+ filtered_args
61
66
  end
62
67
 
63
68
  # Generate error message with tool definition