openclacky 0.6.1 → 0.6.3
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 +26 -0
- data/README.md +39 -88
- data/homebrew/README.md +96 -0
- data/homebrew/openclacky.rb +24 -0
- data/lib/clacky/agent.rb +557 -122
- data/lib/clacky/cli.rb +431 -3
- data/lib/clacky/default_skills/skill-add/SKILL.md +66 -0
- data/lib/clacky/skill.rb +236 -0
- data/lib/clacky/skill_loader.rb +320 -0
- data/lib/clacky/tools/file_reader.rb +245 -9
- data/lib/clacky/tools/grep.rb +9 -14
- data/lib/clacky/tools/safe_shell.rb +53 -17
- data/lib/clacky/tools/shell.rb +109 -5
- data/lib/clacky/tools/web_fetch.rb +81 -18
- data/lib/clacky/ui2/components/command_suggestions.rb +273 -0
- data/lib/clacky/ui2/components/inline_input.rb +34 -15
- data/lib/clacky/ui2/components/input_area.rb +279 -141
- data/lib/clacky/ui2/layout_manager.rb +147 -67
- data/lib/clacky/ui2/line_editor.rb +142 -2
- data/lib/clacky/ui2/themes/hacker_theme.rb +3 -3
- data/lib/clacky/ui2/themes/minimal_theme.rb +3 -3
- data/lib/clacky/ui2/ui_controller.rb +80 -29
- data/lib/clacky/ui2.rb +0 -1
- data/lib/clacky/utils/arguments_parser.rb +7 -2
- data/lib/clacky/utils/file_ignore_helper.rb +10 -12
- data/lib/clacky/utils/file_processor.rb +201 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +2 -0
- data/scripts/install.sh +249 -0
- data/scripts/uninstall.sh +146 -0
- metadata +10 -2
- data/lib/clacky/ui2/components/output_area.rb +0 -112
|
@@ -4,6 +4,7 @@ require "pastel"
|
|
|
4
4
|
require "tempfile"
|
|
5
5
|
require_relative "../theme_manager"
|
|
6
6
|
require_relative "../line_editor"
|
|
7
|
+
require_relative "command_suggestions"
|
|
7
8
|
|
|
8
9
|
module Clacky
|
|
9
10
|
module UI2
|
|
@@ -68,6 +69,10 @@ module Clacky
|
|
|
68
69
|
@animation_frame = 0
|
|
69
70
|
@last_animation_update = Time.now
|
|
70
71
|
@working_frames = ["❄", "❅", "❆"]
|
|
72
|
+
|
|
73
|
+
# Command suggestions dropdown
|
|
74
|
+
@command_suggestions = CommandSuggestions.new
|
|
75
|
+
@skill_loader = nil # Will be set via set_skill_loader method
|
|
71
76
|
end
|
|
72
77
|
|
|
73
78
|
# Get current theme from ThemeManager
|
|
@@ -84,8 +89,15 @@ module Clacky
|
|
|
84
89
|
# When paused (InlineInput active), don't take up any space
|
|
85
90
|
return 0 if @paused
|
|
86
91
|
|
|
87
|
-
height =
|
|
88
|
-
|
|
92
|
+
height = 0
|
|
93
|
+
|
|
94
|
+
# Session bar - calculate actual wrapped height
|
|
95
|
+
height += calculate_sessionbar_height
|
|
96
|
+
|
|
97
|
+
# Separator after session bar
|
|
98
|
+
height += 1
|
|
99
|
+
|
|
100
|
+
# Images
|
|
89
101
|
height += @images.size
|
|
90
102
|
|
|
91
103
|
# Calculate height considering wrapped lines
|
|
@@ -101,12 +113,26 @@ module Clacky
|
|
|
101
113
|
height += wrapped_segments.size
|
|
102
114
|
end
|
|
103
115
|
|
|
104
|
-
|
|
116
|
+
# Bottom separator
|
|
117
|
+
height += 1
|
|
118
|
+
|
|
119
|
+
# Command suggestions (rendered above input)
|
|
120
|
+
height += @command_suggestions.required_height if @command_suggestions
|
|
121
|
+
|
|
122
|
+
# Tips and user tips
|
|
105
123
|
height += 1 if @tips_message
|
|
106
124
|
height += 1 if @user_tip
|
|
125
|
+
|
|
107
126
|
height
|
|
108
127
|
end
|
|
109
128
|
|
|
129
|
+
# Set skill loader for command suggestions
|
|
130
|
+
# @param skill_loader [Clacky::SkillLoader] The skill loader instance
|
|
131
|
+
def set_skill_loader(skill_loader)
|
|
132
|
+
@skill_loader = skill_loader
|
|
133
|
+
@command_suggestions.load_skill_commands(skill_loader) if skill_loader
|
|
134
|
+
end
|
|
135
|
+
|
|
110
136
|
# Update session bar info
|
|
111
137
|
# @param working_dir [String] Working directory
|
|
112
138
|
# @param mode [String] Permission mode
|
|
@@ -133,17 +159,65 @@ module Clacky
|
|
|
133
159
|
|
|
134
160
|
old_height = required_height
|
|
135
161
|
|
|
162
|
+
# Handle command suggestions navigation first if visible
|
|
163
|
+
if @command_suggestions.visible
|
|
164
|
+
case key
|
|
165
|
+
when :up_arrow
|
|
166
|
+
@command_suggestions.select_previous
|
|
167
|
+
return { action: nil }
|
|
168
|
+
when :down_arrow
|
|
169
|
+
@command_suggestions.select_next
|
|
170
|
+
return { action: nil }
|
|
171
|
+
when :enter
|
|
172
|
+
# Accept selected command
|
|
173
|
+
if @command_suggestions.has_suggestions?
|
|
174
|
+
selected = @command_suggestions.selected_command_text
|
|
175
|
+
if selected
|
|
176
|
+
# Replace current input with selected command
|
|
177
|
+
@lines = [selected]
|
|
178
|
+
@line_index = 0
|
|
179
|
+
@cursor_position = selected.length
|
|
180
|
+
@command_suggestions.hide
|
|
181
|
+
return { action: nil }
|
|
182
|
+
end
|
|
183
|
+
end
|
|
184
|
+
# Fall through to normal enter handling if no suggestion
|
|
185
|
+
when :escape
|
|
186
|
+
@command_suggestions.hide
|
|
187
|
+
return { action: nil }
|
|
188
|
+
when :tab
|
|
189
|
+
# Tab also accepts the suggestion
|
|
190
|
+
if @command_suggestions.has_suggestions?
|
|
191
|
+
selected = @command_suggestions.selected_command_text
|
|
192
|
+
if selected
|
|
193
|
+
@lines = [selected]
|
|
194
|
+
@line_index = 0
|
|
195
|
+
@cursor_position = selected.length
|
|
196
|
+
@command_suggestions.hide
|
|
197
|
+
return { action: nil }
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
136
203
|
result = case key
|
|
137
204
|
when Hash
|
|
138
205
|
if key[:type] == :rapid_input
|
|
139
206
|
insert_text(key[:text])
|
|
140
207
|
clear_tips
|
|
208
|
+
update_command_suggestions
|
|
141
209
|
end
|
|
142
210
|
{ action: nil }
|
|
143
211
|
when :enter then handle_enter
|
|
144
212
|
when :newline then newline; { action: nil }
|
|
145
|
-
when :backspace
|
|
146
|
-
|
|
213
|
+
when :backspace
|
|
214
|
+
backspace
|
|
215
|
+
update_command_suggestions
|
|
216
|
+
{ action: nil }
|
|
217
|
+
when :delete
|
|
218
|
+
delete_char
|
|
219
|
+
update_command_suggestions
|
|
220
|
+
{ action: nil }
|
|
147
221
|
when :left_arrow, :ctrl_b then cursor_left; { action: nil }
|
|
148
222
|
when :right_arrow, :ctrl_f then cursor_right; { action: nil }
|
|
149
223
|
when :up_arrow then handle_up_arrow
|
|
@@ -157,10 +231,15 @@ module Clacky
|
|
|
157
231
|
when :ctrl_d then handle_ctrl_d
|
|
158
232
|
when :ctrl_v then handle_paste
|
|
159
233
|
when :shift_tab then { action: :toggle_mode }
|
|
160
|
-
when :escape
|
|
234
|
+
when :escape
|
|
235
|
+
if @command_suggestions.visible
|
|
236
|
+
@command_suggestions.hide
|
|
237
|
+
end
|
|
238
|
+
{ action: nil }
|
|
161
239
|
else
|
|
162
240
|
if key.is_a?(String) && key.length >= 1 && key.ord >= 32
|
|
163
241
|
insert_char(key)
|
|
242
|
+
update_command_suggestions
|
|
164
243
|
end
|
|
165
244
|
{ action: nil }
|
|
166
245
|
end
|
|
@@ -202,54 +281,19 @@ module Clacky
|
|
|
202
281
|
end
|
|
203
282
|
|
|
204
283
|
# Input lines with auto-wrap support
|
|
205
|
-
|
|
206
|
-
prefix = if idx == 0
|
|
207
|
-
prompt_text = theme.format_symbol(:user) + " "
|
|
208
|
-
prompt_text
|
|
209
|
-
else
|
|
210
|
-
" " * prompt.length
|
|
211
|
-
end
|
|
212
|
-
|
|
213
|
-
# Calculate available width for text (excluding prefix)
|
|
214
|
-
prefix_width = calculate_display_width(strip_ansi_codes(prefix))
|
|
215
|
-
available_width = @width - prefix_width
|
|
216
|
-
|
|
217
|
-
# Wrap line if needed
|
|
218
|
-
wrapped_segments = wrap_line(line, available_width)
|
|
219
|
-
|
|
220
|
-
wrapped_segments.each_with_index do |segment_info, wrap_idx|
|
|
221
|
-
move_cursor(current_row, 0)
|
|
222
|
-
|
|
223
|
-
segment_text = segment_info[:text]
|
|
224
|
-
segment_start = segment_info[:start]
|
|
225
|
-
segment_end = segment_info[:end]
|
|
226
|
-
|
|
227
|
-
content = if wrap_idx == 0
|
|
228
|
-
# First wrapped line includes prefix
|
|
229
|
-
if idx == @line_index
|
|
230
|
-
"#{prefix}#{render_line_segment_with_cursor(line, segment_start, segment_end)}"
|
|
231
|
-
else
|
|
232
|
-
"#{prefix}#{theme.format_text(segment_text, :user)}"
|
|
233
|
-
end
|
|
234
|
-
else
|
|
235
|
-
# Continuation lines have indent matching prefix width
|
|
236
|
-
continuation_indent = " " * prefix_width
|
|
237
|
-
if idx == @line_index
|
|
238
|
-
"#{continuation_indent}#{render_line_segment_with_cursor(line, segment_start, segment_end)}"
|
|
239
|
-
else
|
|
240
|
-
"#{continuation_indent}#{theme.format_text(segment_text, :user)}"
|
|
241
|
-
end
|
|
242
|
-
end
|
|
243
|
-
|
|
244
|
-
print_with_padding(content)
|
|
245
|
-
current_row += 1
|
|
246
|
-
end
|
|
247
|
-
end
|
|
284
|
+
current_row = render_input_lines(current_row)
|
|
248
285
|
|
|
249
286
|
# Bottom separator
|
|
250
287
|
render_separator(current_row)
|
|
251
288
|
current_row += 1
|
|
252
289
|
|
|
290
|
+
# Command suggestions (rendered above tips)
|
|
291
|
+
if @command_suggestions && @command_suggestions.visible
|
|
292
|
+
# Render suggestions at current row
|
|
293
|
+
print @command_suggestions.render(row: current_row, col: 0, width: [@width - 4, 60].min)
|
|
294
|
+
current_row += @command_suggestions.required_height
|
|
295
|
+
end
|
|
296
|
+
|
|
253
297
|
# Tips bar (if any)
|
|
254
298
|
if @tips_message
|
|
255
299
|
move_cursor(current_row, 0)
|
|
@@ -526,6 +570,7 @@ module Clacky
|
|
|
526
570
|
@paste_counter = 0
|
|
527
571
|
@paste_placeholders = {}
|
|
528
572
|
clear_tips
|
|
573
|
+
@command_suggestions.hide if @command_suggestions
|
|
529
574
|
end
|
|
530
575
|
|
|
531
576
|
def submit
|
|
@@ -561,75 +606,104 @@ module Clacky
|
|
|
561
606
|
|
|
562
607
|
private
|
|
563
608
|
|
|
564
|
-
#
|
|
565
|
-
#
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
609
|
+
# Update command suggestions based on current input
|
|
610
|
+
# Shows suggestions when input starts with /
|
|
611
|
+
private def update_command_suggestions
|
|
612
|
+
return unless @command_suggestions
|
|
613
|
+
|
|
614
|
+
current = current_line.strip
|
|
615
|
+
|
|
616
|
+
# Check if we should show suggestions (input starts with /)
|
|
617
|
+
if current.start_with?('/') && @line_index == 0
|
|
618
|
+
# Extract the filter text (everything after /)
|
|
619
|
+
filter_text = current[1..-1] || ""
|
|
620
|
+
@command_suggestions.show(filter_text)
|
|
621
|
+
else
|
|
622
|
+
@command_suggestions.hide
|
|
623
|
+
end
|
|
624
|
+
end
|
|
572
625
|
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
626
|
+
# Render all input lines with auto-wrap support
|
|
627
|
+
# @param start_row [Integer] Starting row position
|
|
628
|
+
# @return [Integer] Next available row after rendering all lines
|
|
629
|
+
def render_input_lines(start_row)
|
|
630
|
+
current_row = start_row
|
|
631
|
+
|
|
632
|
+
@lines.each_with_index do |line, line_idx|
|
|
633
|
+
prefix = calculate_line_prefix(line_idx)
|
|
634
|
+
prefix_width = calculate_display_width(strip_ansi_codes(prefix))
|
|
635
|
+
available_width = @width - prefix_width
|
|
636
|
+
wrapped_segments = wrap_line(line, available_width)
|
|
578
637
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
segments << {
|
|
585
|
-
text: chars[segment_start...current_end].join,
|
|
586
|
-
start: segment_start,
|
|
587
|
-
end: current_end
|
|
588
|
-
}
|
|
589
|
-
segment_start = idx
|
|
590
|
-
current_end = idx + 1
|
|
591
|
-
current_width = char_width
|
|
592
|
-
else
|
|
593
|
-
current_end = idx + 1
|
|
594
|
-
current_width += char_width
|
|
638
|
+
wrapped_segments.each_with_index do |segment_info, wrap_idx|
|
|
639
|
+
content = render_line_segment(line, line_idx, segment_info, wrap_idx, prefix, prefix_width)
|
|
640
|
+
move_cursor(current_row, 0)
|
|
641
|
+
print_with_padding(content)
|
|
642
|
+
current_row += 1
|
|
595
643
|
end
|
|
596
644
|
end
|
|
645
|
+
|
|
646
|
+
current_row
|
|
647
|
+
end
|
|
597
648
|
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
649
|
+
# Calculate the prefix (prompt or indent) for a given line index
|
|
650
|
+
# @param line_idx [Integer] Index of the line
|
|
651
|
+
# @return [String] Prefix string (with formatting)
|
|
652
|
+
private def calculate_line_prefix(line_idx)
|
|
653
|
+
if line_idx == 0
|
|
654
|
+
theme.format_symbol(:user) + " "
|
|
655
|
+
else
|
|
656
|
+
" " * prompt.length
|
|
605
657
|
end
|
|
658
|
+
end
|
|
659
|
+
|
|
660
|
+
# Render a single segment of a line (handling cursor and wrapping)
|
|
661
|
+
# @param line [String] Full line text
|
|
662
|
+
# @param line_idx [Integer] Index of the line in @lines
|
|
663
|
+
# @param segment_info [Hash] Segment information from wrap_line
|
|
664
|
+
# @param wrap_idx [Integer] Index of this segment in wrapped segments
|
|
665
|
+
# @param prefix [String] Line prefix (prompt or indent)
|
|
666
|
+
# @param prefix_width [Integer] Display width of the prefix
|
|
667
|
+
# @return [String] Formatted content for this segment
|
|
668
|
+
private def render_line_segment(line, line_idx, segment_info, wrap_idx, prefix, prefix_width)
|
|
669
|
+
segment_text = segment_info[:text]
|
|
670
|
+
segment_start = segment_info[:start]
|
|
671
|
+
segment_end = segment_info[:end]
|
|
606
672
|
|
|
607
|
-
|
|
673
|
+
is_current_line = (line_idx == @line_index)
|
|
674
|
+
is_first_segment = (wrap_idx == 0)
|
|
675
|
+
|
|
676
|
+
# Determine the line prefix
|
|
677
|
+
line_prefix = if is_first_segment
|
|
678
|
+
prefix
|
|
679
|
+
else
|
|
680
|
+
" " * prefix_width # Continuation indent
|
|
681
|
+
end
|
|
682
|
+
|
|
683
|
+
# Render the segment content (with or without cursor)
|
|
684
|
+
segment_content = if is_current_line
|
|
685
|
+
render_line_segment_with_cursor(line, segment_start, segment_end)
|
|
686
|
+
else
|
|
687
|
+
theme.format_text(segment_text, :user)
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
"#{line_prefix}#{segment_content}"
|
|
691
|
+
end
|
|
692
|
+
|
|
693
|
+
# Wrap a line into multiple segments based on available width
|
|
694
|
+
# Considers display width of characters (multi-byte characters like Chinese)
|
|
695
|
+
# @param line [String] The line to wrap
|
|
696
|
+
# @param max_width [Integer] Maximum display width per wrapped line
|
|
697
|
+
# @return [Array<Hash>] Array of segment info: { text: String, start: Integer, end: Integer }
|
|
698
|
+
def wrap_line(line, max_width)
|
|
699
|
+
super(line, max_width)
|
|
608
700
|
end
|
|
609
701
|
|
|
610
702
|
# Calculate display width of a single character
|
|
611
703
|
# @param char [String] Single character
|
|
612
704
|
# @return [Integer] Display width (1 or 2)
|
|
613
705
|
def char_display_width(char)
|
|
614
|
-
|
|
615
|
-
# East Asian Wide and Fullwidth characters take 2 columns
|
|
616
|
-
if (code >= 0x1100 && code <= 0x115F) ||
|
|
617
|
-
(code >= 0x2329 && code <= 0x232A) ||
|
|
618
|
-
(code >= 0x2E80 && code <= 0x303E) ||
|
|
619
|
-
(code >= 0x3040 && code <= 0xA4CF) ||
|
|
620
|
-
(code >= 0xAC00 && code <= 0xD7A3) ||
|
|
621
|
-
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
622
|
-
(code >= 0xFE10 && code <= 0xFE19) ||
|
|
623
|
-
(code >= 0xFE30 && code <= 0xFE6F) ||
|
|
624
|
-
(code >= 0xFF00 && code <= 0xFF60) ||
|
|
625
|
-
(code >= 0xFFE0 && code <= 0xFFE6) ||
|
|
626
|
-
(code >= 0x1F300 && code <= 0x1F9FF) ||
|
|
627
|
-
(code >= 0x20000 && code <= 0x2FFFD) ||
|
|
628
|
-
(code >= 0x30000 && code <= 0x3FFFD)
|
|
629
|
-
2
|
|
630
|
-
else
|
|
631
|
-
1
|
|
632
|
-
end
|
|
706
|
+
super(char)
|
|
633
707
|
end
|
|
634
708
|
|
|
635
709
|
# Strip ANSI escape codes from a string
|
|
@@ -646,12 +720,29 @@ module Clacky
|
|
|
646
720
|
visible_content = content.gsub(/\e\[[0-9;]*m/, '')
|
|
647
721
|
visible_width = calculate_display_width(visible_content)
|
|
648
722
|
|
|
649
|
-
#
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
723
|
+
# IMPORTANT: If content exceeds screen width, truncate to prevent terminal auto-wrap
|
|
724
|
+
if visible_width > @width
|
|
725
|
+
# Content too long - truncate to fit (loses ANSI colors but prevents wrapping)
|
|
726
|
+
truncate_at = 0
|
|
727
|
+
current_width = 0
|
|
728
|
+
visible_content.each_char.with_index do |char, idx|
|
|
729
|
+
char_width = char_display_width(char)
|
|
730
|
+
break if current_width + char_width + 3 > @width # Reserve 3 for "..."
|
|
731
|
+
current_width += char_width
|
|
732
|
+
truncate_at = idx + 1
|
|
733
|
+
end
|
|
734
|
+
print visible_content[0...truncate_at]
|
|
735
|
+
print "..."
|
|
736
|
+
# Pad remaining
|
|
737
|
+
remaining = @width - current_width - 3
|
|
738
|
+
print " " * remaining if remaining > 0
|
|
739
|
+
else
|
|
740
|
+
# Content fits - print normally
|
|
741
|
+
print content
|
|
742
|
+
# Pad with spaces if needed to clear old content
|
|
743
|
+
remaining = @width - visible_width
|
|
744
|
+
print " " * remaining if remaining > 0
|
|
745
|
+
end
|
|
655
746
|
end
|
|
656
747
|
|
|
657
748
|
def handle_enter
|
|
@@ -661,7 +752,7 @@ module Clacky
|
|
|
661
752
|
if text.start_with?('/')
|
|
662
753
|
# Check if it's a command (single slash followed by English letters only)
|
|
663
754
|
# Paths like /xxx/xxxx should not be treated as commands
|
|
664
|
-
if text =~ /^\/([a-zA-Z]+)$/
|
|
755
|
+
if text =~ /^\/([a-zA-Z-]+)$/
|
|
665
756
|
case text
|
|
666
757
|
when '/clear'
|
|
667
758
|
clear
|
|
@@ -671,8 +762,8 @@ module Clacky
|
|
|
671
762
|
when '/exit', '/quit'
|
|
672
763
|
return { action: :exit }
|
|
673
764
|
else
|
|
674
|
-
|
|
675
|
-
|
|
765
|
+
# Let other commands (like skills) pass through to agent
|
|
766
|
+
# Fall through to submit
|
|
676
767
|
end
|
|
677
768
|
end
|
|
678
769
|
# If it's not a command pattern (e.g., /xxx/xxxx), treat as normal input
|
|
@@ -926,44 +1017,52 @@ module Clacky
|
|
|
926
1017
|
# @param segment_end [Integer] End position of segment in line (char index)
|
|
927
1018
|
# @return [String] Rendered segment with cursor if applicable
|
|
928
1019
|
def render_line_segment_with_cursor(line, segment_start, segment_end)
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
if @cursor_position >= segment_start && @cursor_position < segment_end
|
|
934
|
-
# Cursor is in this segment
|
|
935
|
-
cursor_pos_in_segment = @cursor_position - segment_start
|
|
936
|
-
before_cursor = segment_chars[0...cursor_pos_in_segment].join
|
|
937
|
-
cursor_char = segment_chars[cursor_pos_in_segment] || " "
|
|
938
|
-
after_cursor = segment_chars[(cursor_pos_in_segment + 1)..-1]&.join || ""
|
|
939
|
-
|
|
940
|
-
"#{@pastel.white(before_cursor)}#{@pastel.on_white(@pastel.black(cursor_char))}#{@pastel.white(after_cursor)}"
|
|
941
|
-
elsif @cursor_position == segment_end && segment_end == line.length
|
|
942
|
-
# Cursor is at the very end of the line, show it in last segment
|
|
943
|
-
segment_text = segment_chars.join
|
|
944
|
-
"#{@pastel.white(segment_text)}#{@pastel.on_white(@pastel.black(' '))}"
|
|
945
|
-
else
|
|
946
|
-
# Cursor is not in this segment, just format normally
|
|
947
|
-
theme.format_text(segment_chars.join, :user)
|
|
948
|
-
end
|
|
1020
|
+
# Delegate to LineEditor's shared implementation
|
|
1021
|
+
rendered = super(line, segment_start, segment_end)
|
|
1022
|
+
# Apply theme colors for InputArea
|
|
1023
|
+
theme.format_text(rendered, :user)
|
|
949
1024
|
end
|
|
950
1025
|
|
|
1026
|
+
# Render a separator line (ensures it doesn't exceed screen width)
|
|
1027
|
+
# @param row [Integer] Row position to render
|
|
951
1028
|
def render_separator(row)
|
|
952
1029
|
move_cursor(row, 0)
|
|
953
|
-
|
|
954
|
-
|
|
1030
|
+
# Ensure separator doesn't exceed screen width to prevent wrapping
|
|
1031
|
+
separator_width = [@width, 1].max
|
|
1032
|
+
content = @pastel.dim("─" * separator_width)
|
|
1033
|
+
print content
|
|
1034
|
+
# Clear any remaining space
|
|
1035
|
+
remaining = @width - separator_width
|
|
1036
|
+
print " " * remaining if remaining > 0
|
|
955
1037
|
end
|
|
956
1038
|
|
|
1039
|
+
# Render session bar with wrapping support
|
|
1040
|
+
# @param row [Integer] Starting row position
|
|
1041
|
+
# @return [Integer] Number of rows actually used
|
|
957
1042
|
def render_sessionbar(row)
|
|
958
1043
|
move_cursor(row, 0)
|
|
959
1044
|
|
|
960
1045
|
# If no sessionbar info, just render a separator
|
|
961
1046
|
unless @sessionbar_info[:working_dir]
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
1047
|
+
separator_width = [@width, 1].max
|
|
1048
|
+
content = @pastel.dim("─" * separator_width)
|
|
1049
|
+
print content
|
|
1050
|
+
remaining = @width - separator_width
|
|
1051
|
+
print " " * remaining if remaining > 0
|
|
1052
|
+
return 1
|
|
965
1053
|
end
|
|
966
1054
|
|
|
1055
|
+
session_line = build_sessionbar_content
|
|
1056
|
+
|
|
1057
|
+
# IMPORTANT: Always use print_with_padding which handles truncation
|
|
1058
|
+
# to prevent terminal auto-wrap
|
|
1059
|
+
print_with_padding(session_line)
|
|
1060
|
+
1
|
|
1061
|
+
end
|
|
1062
|
+
|
|
1063
|
+
# Build the session bar content string
|
|
1064
|
+
# @return [String] Formatted session bar content
|
|
1065
|
+
private def build_sessionbar_content
|
|
967
1066
|
parts = []
|
|
968
1067
|
separator = @pastel.dim(" │ ")
|
|
969
1068
|
|
|
@@ -998,8 +1097,47 @@ module Clacky
|
|
|
998
1097
|
cost_display = format("$%.1f", @sessionbar_info[:cost])
|
|
999
1098
|
parts << @pastel.dim(@pastel.white(cost_display))
|
|
1000
1099
|
|
|
1001
|
-
|
|
1002
|
-
|
|
1100
|
+
" " + parts.join(separator)
|
|
1101
|
+
end
|
|
1102
|
+
|
|
1103
|
+
# Truncate session bar content to fit within max length
|
|
1104
|
+
# @param content [String] Full session bar content with ANSI codes
|
|
1105
|
+
# @param max_length [Integer] Maximum visible length
|
|
1106
|
+
# @return [String] Truncated content
|
|
1107
|
+
private def truncate_sessionbar_content(content, max_length)
|
|
1108
|
+
# Strip ANSI codes to calculate visible length
|
|
1109
|
+
visible_content = strip_ansi_codes(content)
|
|
1110
|
+
visible_width = calculate_display_width(visible_content)
|
|
1111
|
+
|
|
1112
|
+
return content if visible_width <= max_length
|
|
1113
|
+
|
|
1114
|
+
# Truncate from the end with "..." indicator
|
|
1115
|
+
chars = visible_content.chars
|
|
1116
|
+
current_width = 0
|
|
1117
|
+
truncate_at = 0
|
|
1118
|
+
|
|
1119
|
+
chars.each_with_index do |char, idx|
|
|
1120
|
+
char_width = char_display_width(char)
|
|
1121
|
+
if current_width + char_width + 3 > max_length # Reserve 3 for "..."
|
|
1122
|
+
truncate_at = idx
|
|
1123
|
+
break
|
|
1124
|
+
end
|
|
1125
|
+
current_width += char_width
|
|
1126
|
+
truncate_at = idx + 1
|
|
1127
|
+
end
|
|
1128
|
+
|
|
1129
|
+
# For simplicity with ANSI codes, just show first part + ...
|
|
1130
|
+
# This is a simplified version - proper implementation would preserve ANSI codes
|
|
1131
|
+
visible_content[0...truncate_at] + "..."
|
|
1132
|
+
end
|
|
1133
|
+
|
|
1134
|
+
# Calculate how many rows the session bar will occupy
|
|
1135
|
+
# @return [Integer] Number of rows needed
|
|
1136
|
+
private def calculate_sessionbar_height
|
|
1137
|
+
return 1 unless @sessionbar_info[:working_dir]
|
|
1138
|
+
|
|
1139
|
+
# Session bar always renders on one line (we truncate if needed)
|
|
1140
|
+
1
|
|
1003
1141
|
end
|
|
1004
1142
|
|
|
1005
1143
|
def shorten_path(path)
|