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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +39 -88
- data/homebrew/README.md +96 -0
- data/homebrew/openclacky.rb +24 -0
- data/lib/clacky/agent.rb +139 -67
- data/lib/clacky/cli.rb +105 -6
- data/lib/clacky/tools/file_reader.rb +135 -2
- data/lib/clacky/tools/glob.rb +2 -2
- data/lib/clacky/tools/grep.rb +2 -2
- data/lib/clacky/tools/run_project.rb +5 -5
- data/lib/clacky/tools/safe_shell.rb +140 -17
- data/lib/clacky/tools/shell.rb +69 -2
- data/lib/clacky/tools/todo_manager.rb +50 -3
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/web_fetch.rb +2 -2
- data/lib/clacky/tools/web_search.rb +2 -2
- data/lib/clacky/ui2/components/common_component.rb +14 -5
- data/lib/clacky/ui2/components/input_area.rb +300 -89
- data/lib/clacky/ui2/components/message_component.rb +7 -3
- data/lib/clacky/ui2/components/todo_area.rb +38 -45
- data/lib/clacky/ui2/components/welcome_banner.rb +10 -0
- data/lib/clacky/ui2/layout_manager.rb +180 -50
- data/lib/clacky/ui2/markdown_renderer.rb +80 -0
- data/lib/clacky/ui2/screen_buffer.rb +26 -7
- data/lib/clacky/ui2/themes/base_theme.rb +32 -46
- data/lib/clacky/ui2/themes/hacker_theme.rb +4 -2
- data/lib/clacky/ui2/themes/minimal_theme.rb +4 -2
- data/lib/clacky/ui2/ui_controller.rb +150 -32
- data/lib/clacky/ui2/view_renderer.rb +21 -4
- data/lib/clacky/ui2.rb +0 -1
- data/lib/clacky/utils/arguments_parser.rb +7 -2
- data/lib/clacky/utils/file_processor.rb +201 -0
- data/lib/clacky/version.rb +1 -1
- data/scripts/install.sh +249 -0
- data/scripts/uninstall.sh +146 -0
- metadata +21 -2
- data/lib/clacky/ui2/components/output_area.rb +0 -112
|
@@ -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
|
|
|
@@ -69,8 +84,15 @@ module Clacky
|
|
|
69
84
|
# When paused (InlineInput active), don't take up any space
|
|
70
85
|
return 0 if @paused
|
|
71
86
|
|
|
72
|
-
height =
|
|
73
|
-
|
|
87
|
+
height = 0
|
|
88
|
+
|
|
89
|
+
# Session bar - calculate actual wrapped height
|
|
90
|
+
height += calculate_sessionbar_height
|
|
91
|
+
|
|
92
|
+
# Separator after session bar
|
|
93
|
+
height += 1
|
|
94
|
+
|
|
95
|
+
# Images
|
|
74
96
|
height += @images.size
|
|
75
97
|
|
|
76
98
|
# Calculate height considering wrapped lines
|
|
@@ -86,8 +108,13 @@ module Clacky
|
|
|
86
108
|
height += wrapped_segments.size
|
|
87
109
|
end
|
|
88
110
|
|
|
89
|
-
|
|
111
|
+
# Bottom separator
|
|
112
|
+
height += 1
|
|
113
|
+
|
|
114
|
+
# Tips and user tips
|
|
90
115
|
height += 1 if @tips_message
|
|
116
|
+
height += 1 if @user_tip
|
|
117
|
+
|
|
91
118
|
height
|
|
92
119
|
end
|
|
93
120
|
|
|
@@ -186,49 +213,7 @@ module Clacky
|
|
|
186
213
|
end
|
|
187
214
|
|
|
188
215
|
# Input lines with auto-wrap support
|
|
189
|
-
|
|
190
|
-
prefix = if idx == 0
|
|
191
|
-
prompt_text = theme.format_symbol(:user) + " "
|
|
192
|
-
prompt_text
|
|
193
|
-
else
|
|
194
|
-
" " * prompt.length
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
# Calculate available width for text (excluding prefix)
|
|
198
|
-
prefix_width = calculate_display_width(strip_ansi_codes(prefix))
|
|
199
|
-
available_width = @width - prefix_width
|
|
200
|
-
|
|
201
|
-
# Wrap line if needed
|
|
202
|
-
wrapped_segments = wrap_line(line, available_width)
|
|
203
|
-
|
|
204
|
-
wrapped_segments.each_with_index do |segment_info, wrap_idx|
|
|
205
|
-
move_cursor(current_row, 0)
|
|
206
|
-
|
|
207
|
-
segment_text = segment_info[:text]
|
|
208
|
-
segment_start = segment_info[:start]
|
|
209
|
-
segment_end = segment_info[:end]
|
|
210
|
-
|
|
211
|
-
content = if wrap_idx == 0
|
|
212
|
-
# First wrapped line includes prefix
|
|
213
|
-
if idx == @line_index
|
|
214
|
-
"#{prefix}#{render_line_segment_with_cursor(line, segment_start, segment_end)}"
|
|
215
|
-
else
|
|
216
|
-
"#{prefix}#{theme.format_text(segment_text, :user)}"
|
|
217
|
-
end
|
|
218
|
-
else
|
|
219
|
-
# Continuation lines have indent matching prefix width
|
|
220
|
-
continuation_indent = " " * prefix_width
|
|
221
|
-
if idx == @line_index
|
|
222
|
-
"#{continuation_indent}#{render_line_segment_with_cursor(line, segment_start, segment_end)}"
|
|
223
|
-
else
|
|
224
|
-
"#{continuation_indent}#{theme.format_text(segment_text, :user)}"
|
|
225
|
-
end
|
|
226
|
-
end
|
|
227
|
-
|
|
228
|
-
print_with_padding(content)
|
|
229
|
-
current_row += 1
|
|
230
|
-
end
|
|
231
|
-
end
|
|
216
|
+
current_row = render_input_lines(current_row)
|
|
232
217
|
|
|
233
218
|
# Bottom separator
|
|
234
219
|
render_separator(current_row)
|
|
@@ -242,6 +227,14 @@ module Clacky
|
|
|
242
227
|
current_row += 1
|
|
243
228
|
end
|
|
244
229
|
|
|
230
|
+
# User tip (if any)
|
|
231
|
+
if @user_tip
|
|
232
|
+
move_cursor(current_row, 0)
|
|
233
|
+
content = format_user_tip(@user_tip)
|
|
234
|
+
print_with_padding(content)
|
|
235
|
+
current_row += 1
|
|
236
|
+
end
|
|
237
|
+
|
|
245
238
|
# Position cursor at current edit position
|
|
246
239
|
position_cursor(start_row)
|
|
247
240
|
flush
|
|
@@ -334,6 +327,58 @@ module Clacky
|
|
|
334
327
|
@tips_message = nil
|
|
335
328
|
end
|
|
336
329
|
|
|
330
|
+
# Show a random user tip with probability and auto-rotation (max 3 tips)
|
|
331
|
+
# @param probability [Float] Probability of showing tip (0.0 to 1.0, default: 0.4)
|
|
332
|
+
# @param rotation_interval [Integer] Seconds between tip rotation (default: 12)
|
|
333
|
+
# @param max_tips [Integer] Maximum number of tips to show before stopping (default: 3)
|
|
334
|
+
def show_user_tip(probability: 0.4, rotation_interval: 12, max_tips: 3)
|
|
335
|
+
# Random chance to show tip
|
|
336
|
+
return unless rand < probability
|
|
337
|
+
|
|
338
|
+
# Stop existing timer if any
|
|
339
|
+
stop_user_tip_timer
|
|
340
|
+
|
|
341
|
+
# Reset counter and pick first random tip
|
|
342
|
+
@user_tip_count = 1
|
|
343
|
+
@user_tip = USER_TIPS.sample
|
|
344
|
+
|
|
345
|
+
# Start rotation timer (will show max_tips total)
|
|
346
|
+
@user_tip_timer = Thread.new do
|
|
347
|
+
while @user_tip_count < max_tips
|
|
348
|
+
sleep rotation_interval
|
|
349
|
+
@user_tip_count += 1
|
|
350
|
+
|
|
351
|
+
# Pick a different tip
|
|
352
|
+
old_tip = @user_tip
|
|
353
|
+
loop do
|
|
354
|
+
@user_tip = USER_TIPS.sample
|
|
355
|
+
break if @user_tip != old_tip || USER_TIPS.size == 1
|
|
356
|
+
end
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
# After showing max_tips, wait then clear
|
|
360
|
+
sleep rotation_interval
|
|
361
|
+
@user_tip = nil
|
|
362
|
+
@user_tip_count = 0
|
|
363
|
+
rescue => e
|
|
364
|
+
# Silently handle thread errors
|
|
365
|
+
end
|
|
366
|
+
end
|
|
367
|
+
|
|
368
|
+
# Clear user tip and stop rotation
|
|
369
|
+
def clear_user_tip
|
|
370
|
+
stop_user_tip_timer
|
|
371
|
+
@user_tip = nil
|
|
372
|
+
@user_tip_count = 0
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
private def stop_user_tip_timer
|
|
376
|
+
if @user_tip_timer&.alive?
|
|
377
|
+
@user_tip_timer.kill
|
|
378
|
+
@user_tip_timer = nil
|
|
379
|
+
end
|
|
380
|
+
end
|
|
381
|
+
|
|
337
382
|
# Pause input area (when InlineInput is active)
|
|
338
383
|
def pause
|
|
339
384
|
@paused = true
|
|
@@ -485,6 +530,73 @@ module Clacky
|
|
|
485
530
|
|
|
486
531
|
private
|
|
487
532
|
|
|
533
|
+
# Render all input lines with auto-wrap support
|
|
534
|
+
# @param start_row [Integer] Starting row position
|
|
535
|
+
# @return [Integer] Next available row after rendering all lines
|
|
536
|
+
def render_input_lines(start_row)
|
|
537
|
+
current_row = start_row
|
|
538
|
+
|
|
539
|
+
@lines.each_with_index do |line, line_idx|
|
|
540
|
+
prefix = calculate_line_prefix(line_idx)
|
|
541
|
+
prefix_width = calculate_display_width(strip_ansi_codes(prefix))
|
|
542
|
+
available_width = @width - prefix_width
|
|
543
|
+
wrapped_segments = wrap_line(line, available_width)
|
|
544
|
+
|
|
545
|
+
wrapped_segments.each_with_index do |segment_info, wrap_idx|
|
|
546
|
+
content = render_line_segment(line, line_idx, segment_info, wrap_idx, prefix, prefix_width)
|
|
547
|
+
move_cursor(current_row, 0)
|
|
548
|
+
print_with_padding(content)
|
|
549
|
+
current_row += 1
|
|
550
|
+
end
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
current_row
|
|
554
|
+
end
|
|
555
|
+
|
|
556
|
+
# Calculate the prefix (prompt or indent) for a given line index
|
|
557
|
+
# @param line_idx [Integer] Index of the line
|
|
558
|
+
# @return [String] Prefix string (with formatting)
|
|
559
|
+
private def calculate_line_prefix(line_idx)
|
|
560
|
+
if line_idx == 0
|
|
561
|
+
theme.format_symbol(:user) + " "
|
|
562
|
+
else
|
|
563
|
+
" " * prompt.length
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
|
|
567
|
+
# Render a single segment of a line (handling cursor and wrapping)
|
|
568
|
+
# @param line [String] Full line text
|
|
569
|
+
# @param line_idx [Integer] Index of the line in @lines
|
|
570
|
+
# @param segment_info [Hash] Segment information from wrap_line
|
|
571
|
+
# @param wrap_idx [Integer] Index of this segment in wrapped segments
|
|
572
|
+
# @param prefix [String] Line prefix (prompt or indent)
|
|
573
|
+
# @param prefix_width [Integer] Display width of the prefix
|
|
574
|
+
# @return [String] Formatted content for this segment
|
|
575
|
+
private def render_line_segment(line, line_idx, segment_info, wrap_idx, prefix, prefix_width)
|
|
576
|
+
segment_text = segment_info[:text]
|
|
577
|
+
segment_start = segment_info[:start]
|
|
578
|
+
segment_end = segment_info[:end]
|
|
579
|
+
|
|
580
|
+
is_current_line = (line_idx == @line_index)
|
|
581
|
+
is_first_segment = (wrap_idx == 0)
|
|
582
|
+
|
|
583
|
+
# Determine the line prefix
|
|
584
|
+
line_prefix = if is_first_segment
|
|
585
|
+
prefix
|
|
586
|
+
else
|
|
587
|
+
" " * prefix_width # Continuation indent
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
# Render the segment content (with or without cursor)
|
|
591
|
+
segment_content = if is_current_line
|
|
592
|
+
render_line_segment_with_cursor(line, segment_start, segment_end)
|
|
593
|
+
else
|
|
594
|
+
theme.format_text(segment_text, :user)
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
"#{line_prefix}#{segment_content}"
|
|
598
|
+
end
|
|
599
|
+
|
|
488
600
|
# Wrap a line into multiple segments based on available width
|
|
489
601
|
# Considers display width of characters (multi-byte characters like Chinese)
|
|
490
602
|
# @param line [String] The line to wrap
|
|
@@ -570,12 +682,29 @@ module Clacky
|
|
|
570
682
|
visible_content = content.gsub(/\e\[[0-9;]*m/, '')
|
|
571
683
|
visible_width = calculate_display_width(visible_content)
|
|
572
684
|
|
|
573
|
-
#
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
685
|
+
# IMPORTANT: If content exceeds screen width, truncate to prevent terminal auto-wrap
|
|
686
|
+
if visible_width > @width
|
|
687
|
+
# Content too long - truncate to fit (loses ANSI colors but prevents wrapping)
|
|
688
|
+
truncate_at = 0
|
|
689
|
+
current_width = 0
|
|
690
|
+
visible_content.each_char.with_index do |char, idx|
|
|
691
|
+
char_width = char_display_width(char)
|
|
692
|
+
break if current_width + char_width + 3 > @width # Reserve 3 for "..."
|
|
693
|
+
current_width += char_width
|
|
694
|
+
truncate_at = idx + 1
|
|
695
|
+
end
|
|
696
|
+
print visible_content[0...truncate_at]
|
|
697
|
+
print "..."
|
|
698
|
+
# Pad remaining
|
|
699
|
+
remaining = @width - current_width - 3
|
|
700
|
+
print " " * remaining if remaining > 0
|
|
701
|
+
else
|
|
702
|
+
# Content fits - print normally
|
|
703
|
+
print content
|
|
704
|
+
# Pad with spaces if needed to clear old content
|
|
705
|
+
remaining = @width - visible_width
|
|
706
|
+
print " " * remaining if remaining > 0
|
|
707
|
+
end
|
|
579
708
|
end
|
|
580
709
|
|
|
581
710
|
def handle_enter
|
|
@@ -583,18 +712,23 @@ module Clacky
|
|
|
583
712
|
|
|
584
713
|
# Handle commands (with or without slash)
|
|
585
714
|
if text.start_with?('/')
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
715
|
+
# Check if it's a command (single slash followed by English letters only)
|
|
716
|
+
# Paths like /xxx/xxxx should not be treated as commands
|
|
717
|
+
if text =~ /^\/([a-zA-Z]+)$/
|
|
718
|
+
case text
|
|
719
|
+
when '/clear'
|
|
720
|
+
clear
|
|
721
|
+
return { action: :clear_output }
|
|
722
|
+
when '/help'
|
|
723
|
+
return { action: :help }
|
|
724
|
+
when '/exit', '/quit'
|
|
725
|
+
return { action: :exit }
|
|
726
|
+
else
|
|
727
|
+
set_tips("Unknown command: #{text} (Available: /clear, /help, /exit)", type: :warning)
|
|
728
|
+
return { action: nil }
|
|
729
|
+
end
|
|
597
730
|
end
|
|
731
|
+
# If it's not a command pattern (e.g., /xxx/xxxx), treat as normal input
|
|
598
732
|
elsif text == '?'
|
|
599
733
|
return { action: :help }
|
|
600
734
|
elsif text == 'exit' || text == 'quit'
|
|
@@ -867,36 +1001,60 @@ module Clacky
|
|
|
867
1001
|
end
|
|
868
1002
|
end
|
|
869
1003
|
|
|
1004
|
+
# Render a separator line (ensures it doesn't exceed screen width)
|
|
1005
|
+
# @param row [Integer] Row position to render
|
|
870
1006
|
def render_separator(row)
|
|
871
1007
|
move_cursor(row, 0)
|
|
872
|
-
|
|
873
|
-
|
|
1008
|
+
# Ensure separator doesn't exceed screen width to prevent wrapping
|
|
1009
|
+
separator_width = [@width, 1].max
|
|
1010
|
+
content = @pastel.dim("─" * separator_width)
|
|
1011
|
+
print content
|
|
1012
|
+
# Clear any remaining space
|
|
1013
|
+
remaining = @width - separator_width
|
|
1014
|
+
print " " * remaining if remaining > 0
|
|
874
1015
|
end
|
|
875
1016
|
|
|
1017
|
+
# Render session bar with wrapping support
|
|
1018
|
+
# @param row [Integer] Starting row position
|
|
1019
|
+
# @return [Integer] Number of rows actually used
|
|
876
1020
|
def render_sessionbar(row)
|
|
877
1021
|
move_cursor(row, 0)
|
|
878
1022
|
|
|
879
1023
|
# If no sessionbar info, just render a separator
|
|
880
1024
|
unless @sessionbar_info[:working_dir]
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
1025
|
+
separator_width = [@width, 1].max
|
|
1026
|
+
content = @pastel.dim("─" * separator_width)
|
|
1027
|
+
print content
|
|
1028
|
+
remaining = @width - separator_width
|
|
1029
|
+
print " " * remaining if remaining > 0
|
|
1030
|
+
return 1
|
|
884
1031
|
end
|
|
885
1032
|
|
|
1033
|
+
session_line = build_sessionbar_content
|
|
1034
|
+
|
|
1035
|
+
# IMPORTANT: Always use print_with_padding which handles truncation
|
|
1036
|
+
# to prevent terminal auto-wrap
|
|
1037
|
+
print_with_padding(session_line)
|
|
1038
|
+
1
|
|
1039
|
+
end
|
|
1040
|
+
|
|
1041
|
+
# Build the session bar content string
|
|
1042
|
+
# @return [String] Formatted session bar content
|
|
1043
|
+
private def build_sessionbar_content
|
|
886
1044
|
parts = []
|
|
887
1045
|
separator = @pastel.dim(" │ ")
|
|
888
1046
|
|
|
889
1047
|
# Workspace status with animation
|
|
890
1048
|
if @sessionbar_info[:status]
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
parts << "#{status_indicator} #{
|
|
1049
|
+
status_color = status_color_for(@sessionbar_info[:status])
|
|
1050
|
+
status_indicator = get_status_indicator(@sessionbar_info[:status], status_color)
|
|
1051
|
+
parts << "#{status_indicator} #{@pastel.public_send(status_color, @sessionbar_info[:status])}"
|
|
894
1052
|
end
|
|
895
1053
|
|
|
896
1054
|
# Working directory (shortened if too long)
|
|
897
1055
|
if @sessionbar_info[:working_dir]
|
|
898
1056
|
dir_display = shorten_path(@sessionbar_info[:working_dir])
|
|
899
|
-
parts << @pastel.
|
|
1057
|
+
parts << @pastel.dim(@pastel.cyan(dir_display))
|
|
900
1058
|
end
|
|
901
1059
|
|
|
902
1060
|
# Permission mode
|
|
@@ -907,18 +1065,57 @@ module Clacky
|
|
|
907
1065
|
|
|
908
1066
|
# Model
|
|
909
1067
|
if @sessionbar_info[:model]
|
|
910
|
-
parts << @pastel.
|
|
1068
|
+
parts << @pastel.dim(@pastel.white(@sessionbar_info[:model]))
|
|
911
1069
|
end
|
|
912
1070
|
|
|
913
1071
|
# Tasks count
|
|
914
|
-
parts << @pastel.
|
|
1072
|
+
parts << @pastel.dim(@pastel.white("#{@sessionbar_info[:tasks]} tasks"))
|
|
915
1073
|
|
|
916
1074
|
# Cost
|
|
917
1075
|
cost_display = format("$%.1f", @sessionbar_info[:cost])
|
|
918
|
-
parts << @pastel.
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1076
|
+
parts << @pastel.dim(@pastel.white(cost_display))
|
|
1077
|
+
|
|
1078
|
+
" " + parts.join(separator)
|
|
1079
|
+
end
|
|
1080
|
+
|
|
1081
|
+
# Truncate session bar content to fit within max length
|
|
1082
|
+
# @param content [String] Full session bar content with ANSI codes
|
|
1083
|
+
# @param max_length [Integer] Maximum visible length
|
|
1084
|
+
# @return [String] Truncated content
|
|
1085
|
+
private def truncate_sessionbar_content(content, max_length)
|
|
1086
|
+
# Strip ANSI codes to calculate visible length
|
|
1087
|
+
visible_content = strip_ansi_codes(content)
|
|
1088
|
+
visible_width = calculate_display_width(visible_content)
|
|
1089
|
+
|
|
1090
|
+
return content if visible_width <= max_length
|
|
1091
|
+
|
|
1092
|
+
# Truncate from the end with "..." indicator
|
|
1093
|
+
chars = visible_content.chars
|
|
1094
|
+
current_width = 0
|
|
1095
|
+
truncate_at = 0
|
|
1096
|
+
|
|
1097
|
+
chars.each_with_index do |char, idx|
|
|
1098
|
+
char_width = char_display_width(char)
|
|
1099
|
+
if current_width + char_width + 3 > max_length # Reserve 3 for "..."
|
|
1100
|
+
truncate_at = idx
|
|
1101
|
+
break
|
|
1102
|
+
end
|
|
1103
|
+
current_width += char_width
|
|
1104
|
+
truncate_at = idx + 1
|
|
1105
|
+
end
|
|
1106
|
+
|
|
1107
|
+
# For simplicity with ANSI codes, just show first part + ...
|
|
1108
|
+
# This is a simplified version - proper implementation would preserve ANSI codes
|
|
1109
|
+
visible_content[0...truncate_at] + "..."
|
|
1110
|
+
end
|
|
1111
|
+
|
|
1112
|
+
# Calculate how many rows the session bar will occupy
|
|
1113
|
+
# @return [Integer] Number of rows needed
|
|
1114
|
+
private def calculate_sessionbar_height
|
|
1115
|
+
return 1 unless @sessionbar_info[:working_dir]
|
|
1116
|
+
|
|
1117
|
+
# Session bar always renders on one line (we truncate if needed)
|
|
1118
|
+
1
|
|
922
1119
|
end
|
|
923
1120
|
|
|
924
1121
|
def shorten_path(path)
|
|
@@ -946,30 +1143,30 @@ module Clacky
|
|
|
946
1143
|
def mode_color_for(mode)
|
|
947
1144
|
case mode.to_s
|
|
948
1145
|
when /auto_approve/
|
|
949
|
-
:
|
|
1146
|
+
:magenta
|
|
950
1147
|
when /confirm_safes/
|
|
951
|
-
:
|
|
1148
|
+
:cyan
|
|
952
1149
|
when /confirm_edits/
|
|
953
|
-
:
|
|
1150
|
+
:green
|
|
954
1151
|
when /plan_only/
|
|
955
|
-
:
|
|
1152
|
+
:blue
|
|
956
1153
|
else
|
|
957
1154
|
:white
|
|
958
1155
|
end
|
|
959
1156
|
end
|
|
960
1157
|
|
|
961
|
-
def
|
|
1158
|
+
def status_color_for(status)
|
|
962
1159
|
case status.to_s.downcase
|
|
963
1160
|
when 'idle'
|
|
964
|
-
:
|
|
1161
|
+
:cyan # Use darker cyan for idle state
|
|
965
1162
|
when 'working'
|
|
966
|
-
:
|
|
1163
|
+
:yellow # Use yellow to highlight working state
|
|
967
1164
|
else
|
|
968
|
-
:
|
|
1165
|
+
:cyan
|
|
969
1166
|
end
|
|
970
1167
|
end
|
|
971
1168
|
|
|
972
|
-
def get_status_indicator(status)
|
|
1169
|
+
def get_status_indicator(status, color)
|
|
973
1170
|
case status.to_s.downcase
|
|
974
1171
|
when 'working'
|
|
975
1172
|
# Update animation frame if enough time has passed
|
|
@@ -978,9 +1175,9 @@ module Clacky
|
|
|
978
1175
|
@animation_frame = (@animation_frame + 1) % @working_frames.length
|
|
979
1176
|
@last_animation_update = now
|
|
980
1177
|
end
|
|
981
|
-
@working_frames[@animation_frame]
|
|
1178
|
+
@pastel.public_send(color, @working_frames[@animation_frame])
|
|
982
1179
|
else
|
|
983
|
-
"●" # Idle indicator
|
|
1180
|
+
@pastel.public_send(color, "●") # Idle indicator with same color as text
|
|
984
1181
|
end
|
|
985
1182
|
end
|
|
986
1183
|
|
|
@@ -1012,6 +1209,20 @@ module Clacky
|
|
|
1012
1209
|
end
|
|
1013
1210
|
end
|
|
1014
1211
|
|
|
1212
|
+
# Format user tip (usage suggestion) with lightbulb icon
|
|
1213
|
+
# @param tip [String] Tip message
|
|
1214
|
+
# @return [String] Formatted tip with styling
|
|
1215
|
+
def format_user_tip(tip)
|
|
1216
|
+
# Limit message length to prevent line wrapping
|
|
1217
|
+
max_length = @width - 5 # Reserve space for icon and margins
|
|
1218
|
+
if tip.length > max_length
|
|
1219
|
+
tip = tip[0...(max_length - 3)] + "..."
|
|
1220
|
+
end
|
|
1221
|
+
|
|
1222
|
+
# Use lightbulb icon and dim cyan color for subtle appearance
|
|
1223
|
+
@pastel.dim(@pastel.cyan("💡 #{tip}"))
|
|
1224
|
+
end
|
|
1225
|
+
|
|
1015
1226
|
def move_cursor(row, col)
|
|
1016
1227
|
print "\e[#{row + 1};#{col + 1}H"
|
|
1017
1228
|
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
|
|
74
|
+
prefix = prefix_newline ? "\n" : ""
|
|
75
|
+
"#{prefix}#{symbol} #{text} #{time_str}".rstrip
|
|
72
76
|
end
|
|
73
77
|
end
|
|
74
78
|
end
|