openclacky 0.6.0 → 0.6.1

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.
@@ -363,7 +363,7 @@ module Clacky
363
363
  when 'help'
364
364
  "❓ Help displayed"
365
365
  else
366
- success ? " #{action} completed" : " #{action} failed"
366
+ success ? "[OK] #{action} completed" : "[Error] #{action} failed"
367
367
  end
368
368
  end
369
369
  end
@@ -149,11 +149,11 @@ module Clacky
149
149
 
150
150
  def format_result(result)
151
151
  if result[:error]
152
- " #{result[:error]}"
152
+ "[Error] #{result[:error]}"
153
153
  else
154
154
  title = result[:title] || 'Untitled'
155
155
  display_title = title.length > 40 ? "#{title[0..37]}..." : title
156
- " Fetched: #{display_title}"
156
+ "[OK] Fetched: #{display_title}"
157
157
  end
158
158
  end
159
159
  end
@@ -139,10 +139,10 @@ module Clacky
139
139
 
140
140
  def format_result(result)
141
141
  if result[:error]
142
- " #{result[:error]}"
142
+ "[Error] #{result[:error]}"
143
143
  else
144
144
  count = result[:count] || 0
145
- " Found #{count} results"
145
+ "[OK] Found #{count} results"
146
146
  end
147
147
  end
148
148
  end
@@ -15,7 +15,7 @@ module Clacky
15
15
  "#{symbol} #{text}"
16
16
  end
17
17
 
18
- # Render progress indicator
18
+ # Render progress indicator (stopped state, gray)
19
19
  # @param message [String] Progress message
20
20
  # @return [String] Progress indicator
21
21
  def render_progress(message)
@@ -24,6 +24,15 @@ module Clacky
24
24
  "#{symbol} #{text}"
25
25
  end
26
26
 
27
+ # Render working indicator (active state, yellow)
28
+ # @param message [String] Progress message
29
+ # @return [String] Working indicator
30
+ def render_working(message)
31
+ symbol = format_symbol(:working)
32
+ text = format_text(message, :working)
33
+ "#{symbol} #{text}"
34
+ end
35
+
27
36
  # Render success message
28
37
  # @param message [String] Success message
29
38
  # @return [String] Success message
@@ -65,22 +74,22 @@ module Clacky
65
74
  lines << @pastel.dim("─" * 60)
66
75
  lines << render_success("Task Complete")
67
76
  lines << ""
68
-
77
+
69
78
  # Display each stat on a separate line
70
79
  lines << " Iterations: #{iterations}"
71
80
  lines << " Cost: $#{cost.round(4)}"
72
81
  lines << " Duration: #{duration.round(1)}s" if duration
73
-
82
+
74
83
  # Display cache information if available
75
84
  if cache_tokens && cache_tokens > 0
76
85
  lines << " Cache Tokens: #{cache_tokens} tokens"
77
86
  end
78
-
87
+
79
88
  if cache_requests && cache_requests > 0
80
89
  hit_rate = cache_hits > 0 ? ((cache_hits.to_f / cache_requests) * 100).round(1) : 0
81
90
  lines << " Cache Requests: #{cache_requests} (#{cache_hits} hits, #{hit_rate}% hit rate)"
82
91
  end
83
-
92
+
84
93
  lines.join("\n")
85
94
  end
86
95
  end
@@ -13,6 +13,16 @@ module Clacky
13
13
  class InputArea
14
14
  include LineEditor
15
15
 
16
+ # User tips pool - can be extended with more tips over time
17
+ USER_TIPS = [
18
+ "Shift+Tab to toggle permission mode (confirm_safes ⇄ auto_approve)",
19
+ "Ctrl+C to interrupt AI execution or clear input",
20
+ "Shift+Enter to create multi-line input",
21
+ "Ctrl+V to paste images (supports up to 3 images)",
22
+ "Ctrl+D to delete pasted images",
23
+ "Use /clear to restart session, /help for commands"
24
+ ].freeze
25
+
16
26
  attr_accessor :row
17
27
  attr_reader :cursor_position, :line_index, :images, :tips_message, :tips_type
18
28
 
@@ -36,6 +46,11 @@ module Clacky
36
46
  @tips_timer = nil
37
47
  @last_render_row = nil
38
48
 
49
+ # User tip (usage suggestion) - separate from system tips
50
+ @user_tip = nil
51
+ @user_tip_timer = nil
52
+ @user_tip_count = 0
53
+
39
54
  # Paused state - when InlineInput is active
40
55
  @paused = false
41
56
 
@@ -88,6 +103,7 @@ module Clacky
88
103
 
89
104
  height += 1 # Bottom separator
90
105
  height += 1 if @tips_message
106
+ height += 1 if @user_tip
91
107
  height
92
108
  end
93
109
 
@@ -242,6 +258,14 @@ module Clacky
242
258
  current_row += 1
243
259
  end
244
260
 
261
+ # User tip (if any)
262
+ if @user_tip
263
+ move_cursor(current_row, 0)
264
+ content = format_user_tip(@user_tip)
265
+ print_with_padding(content)
266
+ current_row += 1
267
+ end
268
+
245
269
  # Position cursor at current edit position
246
270
  position_cursor(start_row)
247
271
  flush
@@ -334,6 +358,58 @@ module Clacky
334
358
  @tips_message = nil
335
359
  end
336
360
 
361
+ # Show a random user tip with probability and auto-rotation (max 3 tips)
362
+ # @param probability [Float] Probability of showing tip (0.0 to 1.0, default: 0.4)
363
+ # @param rotation_interval [Integer] Seconds between tip rotation (default: 12)
364
+ # @param max_tips [Integer] Maximum number of tips to show before stopping (default: 3)
365
+ def show_user_tip(probability: 0.4, rotation_interval: 12, max_tips: 3)
366
+ # Random chance to show tip
367
+ return unless rand < probability
368
+
369
+ # Stop existing timer if any
370
+ stop_user_tip_timer
371
+
372
+ # Reset counter and pick first random tip
373
+ @user_tip_count = 1
374
+ @user_tip = USER_TIPS.sample
375
+
376
+ # Start rotation timer (will show max_tips total)
377
+ @user_tip_timer = Thread.new do
378
+ while @user_tip_count < max_tips
379
+ sleep rotation_interval
380
+ @user_tip_count += 1
381
+
382
+ # Pick a different tip
383
+ old_tip = @user_tip
384
+ loop do
385
+ @user_tip = USER_TIPS.sample
386
+ break if @user_tip != old_tip || USER_TIPS.size == 1
387
+ end
388
+ end
389
+
390
+ # After showing max_tips, wait then clear
391
+ sleep rotation_interval
392
+ @user_tip = nil
393
+ @user_tip_count = 0
394
+ rescue => e
395
+ # Silently handle thread errors
396
+ end
397
+ end
398
+
399
+ # Clear user tip and stop rotation
400
+ def clear_user_tip
401
+ stop_user_tip_timer
402
+ @user_tip = nil
403
+ @user_tip_count = 0
404
+ end
405
+
406
+ private def stop_user_tip_timer
407
+ if @user_tip_timer&.alive?
408
+ @user_tip_timer.kill
409
+ @user_tip_timer = nil
410
+ end
411
+ end
412
+
337
413
  # Pause input area (when InlineInput is active)
338
414
  def pause
339
415
  @paused = true
@@ -583,18 +659,23 @@ module Clacky
583
659
 
584
660
  # Handle commands (with or without slash)
585
661
  if text.start_with?('/')
586
- case text
587
- when '/clear'
588
- clear
589
- return { action: :clear_output }
590
- when '/help'
591
- return { action: :help }
592
- when '/exit', '/quit'
593
- return { action: :exit }
594
- else
595
- set_tips("Unknown command: #{text} (Available: /clear, /help, /exit)", type: :warning)
596
- return { action: nil }
662
+ # Check if it's a command (single slash followed by English letters only)
663
+ # Paths like /xxx/xxxx should not be treated as commands
664
+ if text =~ /^\/([a-zA-Z]+)$/
665
+ case text
666
+ when '/clear'
667
+ clear
668
+ return { action: :clear_output }
669
+ when '/help'
670
+ return { action: :help }
671
+ when '/exit', '/quit'
672
+ return { action: :exit }
673
+ else
674
+ set_tips("Unknown command: #{text} (Available: /clear, /help, /exit)", type: :warning)
675
+ return { action: nil }
676
+ end
597
677
  end
678
+ # If it's not a command pattern (e.g., /xxx/xxxx), treat as normal input
598
679
  elsif text == '?'
599
680
  return { action: :help }
600
681
  elsif text == 'exit' || text == 'quit'
@@ -888,15 +969,15 @@ module Clacky
888
969
 
889
970
  # Workspace status with animation
890
971
  if @sessionbar_info[:status]
891
- status_indicator = get_status_indicator(@sessionbar_info[:status])
892
- status_theme_key = status_theme_key_for(@sessionbar_info[:status])
893
- parts << "#{status_indicator} #{theme.format_text(@sessionbar_info[:status], status_theme_key)}"
972
+ status_color = status_color_for(@sessionbar_info[:status])
973
+ status_indicator = get_status_indicator(@sessionbar_info[:status], status_color)
974
+ parts << "#{status_indicator} #{@pastel.public_send(status_color, @sessionbar_info[:status])}"
894
975
  end
895
976
 
896
977
  # Working directory (shortened if too long)
897
978
  if @sessionbar_info[:working_dir]
898
979
  dir_display = shorten_path(@sessionbar_info[:working_dir])
899
- parts << @pastel.bright_cyan(dir_display)
980
+ parts << @pastel.dim(@pastel.cyan(dir_display))
900
981
  end
901
982
 
902
983
  # Permission mode
@@ -907,15 +988,15 @@ module Clacky
907
988
 
908
989
  # Model
909
990
  if @sessionbar_info[:model]
910
- parts << @pastel.bright_white(@sessionbar_info[:model])
991
+ parts << @pastel.dim(@pastel.white(@sessionbar_info[:model]))
911
992
  end
912
993
 
913
994
  # Tasks count
914
- parts << @pastel.yellow("#{@sessionbar_info[:tasks]} tasks")
995
+ parts << @pastel.dim(@pastel.white("#{@sessionbar_info[:tasks]} tasks"))
915
996
 
916
997
  # Cost
917
998
  cost_display = format("$%.1f", @sessionbar_info[:cost])
918
- parts << @pastel.yellow(cost_display)
999
+ parts << @pastel.dim(@pastel.white(cost_display))
919
1000
 
920
1001
  session_line = " " + parts.join(separator)
921
1002
  print_with_padding(session_line)
@@ -946,30 +1027,30 @@ module Clacky
946
1027
  def mode_color_for(mode)
947
1028
  case mode.to_s
948
1029
  when /auto_approve/
949
- :bright_magenta
1030
+ :magenta
950
1031
  when /confirm_safes/
951
- :bright_yellow
1032
+ :cyan
952
1033
  when /confirm_edits/
953
- :bright_green
1034
+ :green
954
1035
  when /plan_only/
955
- :bright_blue
1036
+ :blue
956
1037
  else
957
1038
  :white
958
1039
  end
959
1040
  end
960
1041
 
961
- def status_theme_key_for(status)
1042
+ def status_color_for(status)
962
1043
  case status.to_s.downcase
963
1044
  when 'idle'
964
- :info # Use info color for idle state
1045
+ :cyan # Use darker cyan for idle state
965
1046
  when 'working'
966
- :progress # Use progress color for working state
1047
+ :yellow # Use yellow to highlight working state
967
1048
  else
968
- :info
1049
+ :cyan
969
1050
  end
970
1051
  end
971
1052
 
972
- def get_status_indicator(status)
1053
+ def get_status_indicator(status, color)
973
1054
  case status.to_s.downcase
974
1055
  when 'working'
975
1056
  # Update animation frame if enough time has passed
@@ -978,9 +1059,9 @@ module Clacky
978
1059
  @animation_frame = (@animation_frame + 1) % @working_frames.length
979
1060
  @last_animation_update = now
980
1061
  end
981
- @working_frames[@animation_frame]
1062
+ @pastel.public_send(color, @working_frames[@animation_frame])
982
1063
  else
983
- "●" # Idle indicator
1064
+ @pastel.public_send(color, "●") # Idle indicator with same color as text
984
1065
  end
985
1066
  end
986
1067
 
@@ -1012,6 +1093,20 @@ module Clacky
1012
1093
  end
1013
1094
  end
1014
1095
 
1096
+ # Format user tip (usage suggestion) with lightbulb icon
1097
+ # @param tip [String] Tip message
1098
+ # @return [String] Formatted tip with styling
1099
+ def format_user_tip(tip)
1100
+ # Limit message length to prevent line wrapping
1101
+ max_length = @width - 5 # Reserve space for icon and margins
1102
+ if tip.length > max_length
1103
+ tip = tip[0...(max_length - 3)] + "..."
1104
+ end
1105
+
1106
+ # Use lightbulb icon and dim cyan color for subtle appearance
1107
+ @pastel.dim(@pastel.cyan("💡 #{tip}"))
1108
+ end
1109
+
1015
1110
  def move_cursor(row, col)
1016
1111
  print "\e[#{row + 1};#{col + 1}H"
1017
1112
  end
@@ -13,12 +13,14 @@ module Clacky
13
13
  # - :content [String] Message content
14
14
  # - :timestamp [Time, nil] Optional timestamp
15
15
  # - :images [Array<String>] Optional image paths (for user messages)
16
+ # - :prefix_newline [Boolean] Whether to add newline before message (for system messages)
16
17
  # @return [String] Rendered message
17
18
  def render(data)
18
19
  role = data[:role]
19
20
  content = data[:content]
20
21
  timestamp = data[:timestamp]
21
22
  images = data[:images] || []
23
+ prefix_newline = data.fetch(:prefix_newline, true)
22
24
 
23
25
  case role
24
26
  when "user"
@@ -26,7 +28,7 @@ module Clacky
26
28
  when "assistant"
27
29
  render_assistant_message(content, timestamp)
28
30
  else
29
- render_system_message(content, timestamp)
31
+ render_system_message(content, timestamp, prefix_newline)
30
32
  end
31
33
  end
32
34
 
@@ -62,13 +64,15 @@ module Clacky
62
64
  # Render system message
63
65
  # @param content [String] Message content
64
66
  # @param timestamp [Time, nil] Optional timestamp
67
+ # @param prefix_newline [Boolean] Whether to add newline before message
65
68
  # @return [String] Rendered message
66
- def render_system_message(content, timestamp = nil)
69
+ private def render_system_message(content, timestamp = nil, prefix_newline = true)
67
70
  symbol = format_symbol(:info)
68
71
  text = format_text(content, :info)
69
72
  time_str = timestamp ? @pastel.dim("[#{format_timestamp(timestamp)}]") : ""
70
73
 
71
- "\n#{symbol} #{text} #{time_str}".rstrip
74
+ prefix = prefix_newline ? "\n" : ""
75
+ "#{prefix}#{symbol} #{text} #{time_str}".rstrip
72
76
  end
73
77
  end
74
78
  end
@@ -21,7 +21,7 @@ module Clacky
21
21
  # Multi-line handling is done by LayoutManager
22
22
  # @param content [String] Single line content to append
23
23
  def append(content)
24
- return if content.nil? || content.empty?
24
+ return if content.nil?
25
25
 
26
26
  update_width
27
27
  print wrap_line(content)
@@ -10,7 +10,7 @@ module Clacky
10
10
  attr_accessor :height
11
11
  attr_reader :todos
12
12
 
13
- MAX_DISPLAY_TASKS = 2 # Show at most 2 tasks (Next + After)
13
+ MAX_DISPLAY_TASKS = 3 # Show current + next 2 tasks
14
14
 
15
15
  def initialize
16
16
  @todos = []
@@ -27,12 +27,11 @@ module Clacky
27
27
  @completed_count = @todos.count { |t| t[:status] == "completed" }
28
28
  @total_count = @todos.size
29
29
 
30
- # Height: 1 line for header + min(pending_count, MAX_DISPLAY_TASKS) lines for tasks
31
- if @pending_todos.empty? && @completed_count == 0
30
+ # Calculate height: 0 if no pending, otherwise 1 line per task (up to MAX_DISPLAY_TASKS)
31
+ if @pending_todos.empty?
32
32
  @height = 0
33
33
  else
34
- display_count = [@pending_todos.size, MAX_DISPLAY_TASKS].min
35
- @height = 1 + display_count
34
+ @height = [@pending_todos.size, MAX_DISPLAY_TASKS].min
36
35
  end
37
36
  end
38
37
 
@@ -48,21 +47,37 @@ module Clacky
48
47
 
49
48
  update_width
50
49
 
51
- # Render header: [##] Tasks [0/4]: ████
52
- move_cursor(start_row, 0)
53
- clear_line
54
- header = render_header
55
- print header
56
-
57
- # Render tasks (Next and After)
58
- @pending_todos.take(MAX_DISPLAY_TASKS).each_with_index do |todo, i|
59
- move_cursor(start_row + i + 1, 0)
60
- clear_line
61
-
62
- label = i == 0 ? "Next" : "After"
63
- task_text = truncate_text("##{todo[:id]} - #{todo[:task]}", @width - 12)
64
- line = " #{@pastel.dim("->")} #{@pastel.yellow(label)}: #{task_text}"
65
- print line
50
+ # Render each task on separate line
51
+ tasks_to_show = @pending_todos.take(MAX_DISPLAY_TASKS)
52
+
53
+ tasks_to_show.each_with_index do |task, index|
54
+ move_cursor(start_row + index, 0)
55
+
56
+ # Build the line content
57
+ line_content = if index == 0
58
+ # First line: Task [2/4]: #3 - Current task description
59
+ progress = "#{@completed_count}/#{@total_count}"
60
+ prefix = "Task [#{progress}]: "
61
+ task_text = "##{task[:id]} - #{task[:task]}"
62
+ available_width = @width - prefix.length - 2
63
+ truncated_task = truncate_text(task_text, available_width)
64
+
65
+ "#{@pastel.cyan(prefix)}#{truncated_task}"
66
+ else
67
+ # Subsequent lines: -> Next: #4 - Next task description
68
+ label = index == 1 ? "Next" : "After"
69
+ prefix = "-> #{label}: "
70
+ task_text = "##{task[:id]} - #{task[:task]}"
71
+ available_width = @width - prefix.length - 2
72
+ truncated_task = truncate_text(task_text, available_width)
73
+
74
+ "#{@pastel.dim(prefix)}#{@pastel.dim(truncated_task)}"
75
+ end
76
+
77
+ # Use carriage return and print content directly (overwrite existing content)
78
+ print "\r#{line_content}"
79
+ # Clear any remaining characters from previous render if line is shorter
80
+ clear_to_end_of_line
66
81
  end
67
82
 
68
83
  flush
@@ -79,28 +94,6 @@ module Clacky
79
94
 
80
95
  private
81
96
 
82
- # Render header line with progress bar
83
- def render_header
84
- progress = "#{@completed_count}/#{@total_count}"
85
- progress_bar = render_progress_bar(@completed_count, @total_count)
86
-
87
- "#{@pastel.cyan("[##]")} Tasks [#{progress}]: #{progress_bar}"
88
- end
89
-
90
- # Render a simple progress bar
91
- def render_progress_bar(completed, total)
92
- return "" if total == 0
93
-
94
- bar_width = 10
95
- filled = total > 0 ? (completed.to_f / total * bar_width).round : 0
96
- empty = bar_width - filled
97
-
98
- filled_bar = @pastel.green("█" * filled)
99
- empty_bar = @pastel.dim("░" * empty)
100
-
101
- "#{filled_bar}#{empty_bar}"
102
- end
103
-
104
97
  # Truncate text to fit width
105
98
  def truncate_text(text, max_width)
106
99
  return "" if text.nil?
@@ -122,9 +115,9 @@ module Clacky
122
115
  print "\e[#{row + 1};#{col + 1}H"
123
116
  end
124
117
 
125
- # Clear current line
126
- def clear_line
127
- print "\e[2K"
118
+ # Clear from cursor to end of line
119
+ def clear_to_end_of_line
120
+ print "\e[0K"
128
121
  end
129
122
 
130
123
  # Flush output
@@ -29,6 +29,16 @@ module Clacky
29
29
  @pastel = Pastel.new
30
30
  end
31
31
 
32
+ # Render only the logo (ASCII art)
33
+ # @return [String] Formatted logo only
34
+ def render_logo
35
+ lines = []
36
+ lines << ""
37
+ lines << @pastel.bright_green(LOGO)
38
+ lines << ""
39
+ lines.join("\n")
40
+ end
41
+
32
42
  # Render startup banner
33
43
  # @return [String] Formatted startup banner
34
44
  def render_startup