openclacky 0.6.2 → 0.6.4

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.
@@ -14,6 +14,7 @@ module Clacky
14
14
  @todo_area = todo_area
15
15
  @render_mutex = Mutex.new
16
16
  @output_row = 0 # Track current output row position
17
+ @last_fixed_area_height = 0 # Track previous fixed area height to detect shrinkage
17
18
 
18
19
  calculate_layout
19
20
  setup_resize_handler
@@ -91,11 +92,19 @@ module Clacky
91
92
  def position_inline_input_cursor(inline_input)
92
93
  return unless inline_input
93
94
 
94
- # Calculate the actual terminal cursor position considering multi-byte characters
95
- # InlineInput is on the last output line (@output_row - 1)
96
- cursor_row = @output_row - 1
97
- cursor_col = inline_input.cursor_col # This already considers display width
98
-
95
+ # Use the shared method from LineEditor to calculate cursor position with wrap
96
+ prompt = inline_input.prompt
97
+ width = screen.width
98
+ wrap_row, wrap_col = inline_input.cursor_position_with_wrap(prompt, width)
99
+
100
+ # Get the number of lines InlineInput occupies (considering wrapping)
101
+ line_count = inline_input.line_count(width)
102
+
103
+ # InlineInput starts at @output_row - line_count
104
+ # Cursor is at wrap_row within that
105
+ cursor_row = @output_row - line_count + wrap_row
106
+ cursor_col = wrap_col
107
+
99
108
  # Move terminal cursor to the correct position
100
109
  screen.move_cursor(cursor_row, cursor_col)
101
110
  screen.flush
@@ -184,11 +193,11 @@ module Clacky
184
193
 
185
194
  @render_mutex.synchronize do
186
195
  lines = content.split("\n", -1) # -1 to keep trailing empty strings
187
-
196
+
188
197
  lines.each_with_index do |line, index|
189
198
  # Wrap long lines to prevent display issues
190
199
  wrapped_lines = wrap_long_line(line)
191
-
200
+
192
201
  wrapped_lines.each do |wrapped_line|
193
202
  write_output_line(wrapped_line)
194
203
  end
@@ -200,40 +209,72 @@ module Clacky
200
209
  end
201
210
  end
202
211
 
203
- # Update the last line in output area (for progress indicator)
204
- # @param content [String] Content to update
205
- def update_last_line(content)
212
+ # Update the last N lines in output area (for inline input updates)
213
+ # @param content [String] Content to update (may contain newlines for wrapped lines)
214
+ # @param old_line_count [Integer] Number of lines currently occupied (for clearing)
215
+ def update_last_line(content, old_line_count = 1)
206
216
  @render_mutex.synchronize do
207
217
  return if @output_row == 0 # No output yet
208
-
209
- # Last written line is at @output_row - 1
210
- last_row = @output_row - 1
211
- screen.move_cursor(last_row, 0)
212
- screen.clear_line
213
- print content
214
-
215
- # Hide terminal cursor to avoid showing two cursors
216
- # InlineInput uses visual cursor (white background) which is better for multi-byte chars
217
- screen.hide_cursor
218
+
219
+ # Calculate start row (last N lines)
220
+ start_row = @output_row - old_line_count
221
+ start_row = 0 if start_row < 0
222
+
223
+ # Clear all lines that will be updated
224
+ (start_row...@output_row).each do |row|
225
+ screen.move_cursor(row, 0)
226
+ screen.clear_line
227
+ end
228
+
229
+ # Re-render the content
230
+ lines = content.split("\n", -1)
231
+ current_row = start_row
232
+
233
+ lines.each_with_index do |line, idx|
234
+ screen.move_cursor(current_row, 0)
235
+ print line
236
+ current_row += 1
237
+ end
238
+
239
+ # Update output_row to new line count
240
+ @output_row = start_row + lines.length
241
+
242
+ # Clear any remaining old lines if new content has fewer lines
243
+ # This handles the case where content shrinks (e.g., delete from 2 lines to 1 line)
244
+ old_end_row = @output_row + (old_line_count - lines.length)
245
+ if old_end_row > @output_row && old_end_row <= start_row + old_line_count
246
+ # Clear the extra old lines
247
+ (@output_row...old_end_row).each do |row|
248
+ screen.move_cursor(row, 0)
249
+ screen.clear_line
250
+ end
251
+ end
252
+
253
+ # Re-render fixed areas to restore cursor position in input area
254
+ render_fixed_areas
218
255
  screen.flush
219
-
220
- # Don't re-render fixed areas - we're just updating existing content
221
256
  end
222
257
  end
223
258
 
224
- # Remove the last line from output area
225
- def remove_last_line
259
+ # Remove the last N lines from output area
260
+ # @param line_count [Integer] Number of lines to remove (default: 1)
261
+ def remove_last_line(line_count = 1)
226
262
  @render_mutex.synchronize do
227
263
  return if @output_row == 0 # No output to remove
228
-
229
- # Clear the last written line
230
- last_row = @output_row - 1
231
- screen.move_cursor(last_row, 0)
232
- screen.clear_line
233
-
234
- # Move output row back
235
- @output_row = last_row
236
-
264
+
265
+ # Calculate start row for removal
266
+ start_row = @output_row - line_count
267
+ start_row = 0 if start_row < 0
268
+
269
+ # Clear all lines being removed
270
+ (start_row...@output_row).each do |row|
271
+ screen.move_cursor(row, 0)
272
+ screen.clear_line
273
+ end
274
+
275
+ # Update output_row
276
+ @output_row = start_row
277
+
237
278
  # Re-render fixed areas to ensure consistency
238
279
  render_fixed_areas
239
280
  screen.flush
@@ -416,11 +457,26 @@ module Clacky
416
457
  # The InlineInput is rendered inline with output
417
458
  return if input_area.paused?
418
459
 
460
+ current_fixed_height = fixed_area_height
419
461
  start_row = fixed_area_start_row
420
462
  gap_row = start_row
421
463
  todo_row = gap_row + 1
422
464
  input_row = todo_row + (@todo_area&.height || 0)
423
465
 
466
+ # If fixed area shrank, clear the extra lines at the top to remove residual content
467
+ if @last_fixed_area_height > current_fixed_height
468
+ height_diff = @last_fixed_area_height - current_fixed_height
469
+ old_start_row = screen.height - @last_fixed_area_height
470
+ # Clear the extra lines that are no longer part of fixed area
471
+ (old_start_row...(old_start_row + height_diff)).each do |row|
472
+ screen.move_cursor(row, 0)
473
+ screen.clear_line
474
+ end
475
+ end
476
+
477
+ # Update last height for next comparison
478
+ @last_fixed_area_height = current_fixed_height
479
+
424
480
  # Render gap line
425
481
  screen.move_cursor(gap_row, 0)
426
482
  screen.clear_line
@@ -188,14 +188,154 @@ module Clacky
188
188
  # Strip ANSI codes from prompt to get actual display width
189
189
  visible_prompt = strip_ansi_codes(prompt)
190
190
  prompt_display_width = calculate_display_width(visible_prompt)
191
-
191
+
192
192
  # Calculate display width of text before cursor
193
193
  chars = @line.chars
194
194
  text_before_cursor = chars[0...@cursor_position].join
195
195
  text_display_width = calculate_display_width(text_before_cursor)
196
-
196
+
197
197
  prompt_display_width + text_display_width
198
198
  end
199
+
200
+ # Get cursor position considering line wrapping
201
+ # @param prompt [String] Prompt string before the line (may contain ANSI codes)
202
+ # @param width [Integer] Terminal width for wrapping
203
+ # @return [Array<Integer>] Row and column position (0-indexed)
204
+ def cursor_position_with_wrap(prompt = "", width = TTY::Screen.width)
205
+ return [0, cursor_column(prompt)] if width <= 0
206
+
207
+ prompt_width = calculate_display_width(strip_ansi_codes(prompt))
208
+ available_width = width - prompt_width
209
+
210
+ # Get wrapped segments for current line
211
+ wrapped_segments = wrap_line(@line, available_width)
212
+
213
+ # Find which segment contains cursor
214
+ cursor_segment_idx = 0
215
+ cursor_pos_in_segment = @cursor_position
216
+
217
+ wrapped_segments.each_with_index do |segment, idx|
218
+ if @cursor_position >= segment[:start] && @cursor_position < segment[:end]
219
+ cursor_segment_idx = idx
220
+ cursor_pos_in_segment = @cursor_position - segment[:start]
221
+ break
222
+ elsif @cursor_position >= segment[:end] && idx == wrapped_segments.size - 1
223
+ cursor_segment_idx = idx
224
+ cursor_pos_in_segment = segment[:end] - segment[:start]
225
+ break
226
+ end
227
+ end
228
+
229
+ # Calculate display width of text before cursor in this segment
230
+ chars = @line.chars
231
+ segment_start = wrapped_segments[cursor_segment_idx][:start]
232
+ text_in_segment_before_cursor = chars[segment_start...(segment_start + cursor_pos_in_segment)].join
233
+ display_width = calculate_display_width(text_in_segment_before_cursor)
234
+
235
+ col = prompt_width + display_width
236
+ row = cursor_segment_idx
237
+
238
+ [row, col]
239
+ end
240
+
241
+ # Wrap a line into multiple segments based on available width
242
+ # Considers display width of characters (multi-byte characters like Chinese)
243
+ # @param line [String] The line to wrap
244
+ # @param max_width [Integer] Maximum display width per wrapped line
245
+ # @return [Array<Hash>] Array of segment info: { text: String, start: Integer, end: Integer }
246
+ def wrap_line(line, max_width)
247
+ return [{ text: "", start: 0, end: 0 }] if line.empty?
248
+ return [{ text: line, start: 0, end: line.length }] if max_width <= 0
249
+
250
+ segments = []
251
+ chars = line.chars
252
+ segment_start = 0
253
+ current_width = 0
254
+ current_end = 0
255
+
256
+ chars.each_with_index do |char, idx|
257
+ char_width = char_display_width(char)
258
+
259
+ # If adding this character exceeds max width, complete current segment
260
+ if current_width + char_width > max_width && current_end > segment_start
261
+ segments << {
262
+ text: chars[segment_start...current_end].join,
263
+ start: segment_start,
264
+ end: current_end
265
+ }
266
+ segment_start = idx
267
+ current_end = idx + 1
268
+ current_width = char_width
269
+ else
270
+ current_end = idx + 1
271
+ current_width += char_width
272
+ end
273
+ end
274
+
275
+ # Add the last segment
276
+ if current_end > segment_start
277
+ segments << {
278
+ text: chars[segment_start...current_end].join,
279
+ start: segment_start,
280
+ end: current_end
281
+ }
282
+ end
283
+
284
+ segments.empty? ? [{ text: "", start: 0, end: 0 }] : segments
285
+ end
286
+
287
+ # Calculate display width of a single character
288
+ # @param char [String] Single character
289
+ # @return [Integer] Display width (1 or 2)
290
+ def char_display_width(char)
291
+ code = char.ord
292
+ # East Asian Wide and Fullwidth characters take 2 columns
293
+ if (code >= 0x1100 && code <= 0x115F) ||
294
+ (code >= 0x2329 && code <= 0x232A) ||
295
+ (code >= 0x2E80 && code <= 0x303E) ||
296
+ (code >= 0x3040 && code <= 0xA4CF) ||
297
+ (code >= 0xAC00 && code <= 0xD7A3) ||
298
+ (code >= 0xF900 && code <= 0xFAFF) ||
299
+ (code >= 0xFE10 && code <= 0xFE19) ||
300
+ (code >= 0xFE30 && code <= 0xFE6F) ||
301
+ (code >= 0xFF00 && code <= 0xFF60) ||
302
+ (code >= 0xFFE0 && code <= 0xFFE6) ||
303
+ (code >= 0x1F300 && code <= 0x1F9FF) ||
304
+ (code >= 0x20000 && code <= 0x2FFFD) ||
305
+ (code >= 0x30000 && code <= 0x3FFFD)
306
+ 2
307
+ else
308
+ 1
309
+ end
310
+ end
311
+
312
+ # Render a segment of a line with cursor if cursor is in this segment
313
+ # @param line [String] Full line text
314
+ # @param segment_start [Integer] Start position of segment in line (char index)
315
+ # @param segment_end [Integer] End position of segment in line (char index)
316
+ # @return [String] Rendered segment with cursor if applicable
317
+ def render_line_segment_with_cursor(line, segment_start, segment_end)
318
+ chars = line.chars
319
+ segment_chars = chars[segment_start...segment_end]
320
+
321
+ # Check if cursor is in this segment
322
+ if @cursor_position >= segment_start && @cursor_position < segment_end
323
+ # Cursor is in this segment
324
+ cursor_pos_in_segment = @cursor_position - segment_start
325
+ before_cursor = segment_chars[0...cursor_pos_in_segment].join
326
+ cursor_char = segment_chars[cursor_pos_in_segment] || " "
327
+ after_cursor = segment_chars[(cursor_pos_in_segment + 1)..-1]&.join || ""
328
+
329
+ "#{@pastel.white(before_cursor)}#{@pastel.on_white(@pastel.black(cursor_char))}#{@pastel.white(after_cursor)}"
330
+ elsif @cursor_position == segment_end && segment_end == line.length
331
+ # Cursor is at the very end of the line, show it in last segment
332
+ segment_text = segment_chars.join
333
+ "#{@pastel.white(segment_text)}#{@pastel.on_white(@pastel.black(' '))}"
334
+ else
335
+ # Cursor is not in this segment, just format normally
336
+ @pastel.white(segment_chars.join)
337
+ end
338
+ end
199
339
  end
200
340
  end
201
341
  end
@@ -32,7 +32,7 @@ module Clacky
32
32
  user: [:white, :white],
33
33
  assistant: [:bright_green, :white],
34
34
  tool_call: [:bright_cyan, :cyan],
35
- tool_result: [:cyan, :white],
35
+ tool_result: [:bright_cyan, :cyan],
36
36
  tool_denied: [:bright_yellow, :yellow],
37
37
  tool_planned: [:bright_cyan, :cyan],
38
38
  tool_error: [:bright_red, :red],
@@ -29,7 +29,7 @@ module Clacky
29
29
  user: [:white, :white],
30
30
  assistant: [:green, :white],
31
31
  tool_call: [:cyan, :cyan],
32
- tool_result: [:white, :white],
32
+ tool_result: [:cyan, :cyan],
33
33
  tool_denied: [:yellow, :yellow],
34
34
  tool_planned: [:cyan, :cyan],
35
35
  tool_error: [:red, :red],
@@ -147,6 +147,12 @@ module Clacky
147
147
  @mode_toggle_callback = block
148
148
  end
149
149
 
150
+ # Set skill loader for command suggestions
151
+ # @param skill_loader [Clacky::SkillLoader] The skill loader instance
152
+ def set_skill_loader(skill_loader)
153
+ @input_area.set_skill_loader(skill_loader)
154
+ end
155
+
150
156
  # Append output to the output area
151
157
  # @param content [String] Content to append
152
158
  def append_output(content)
@@ -214,7 +220,7 @@ module Clacky
214
220
 
215
221
  # Delta tokens with color coding (green/yellow/red + dim)
216
222
  delta_tokens = token_data[:delta_tokens]
217
- delta_str = "+#{delta_tokens}"
223
+ delta_str = delta_tokens.negative? ? "#{delta_tokens}" : "+#{delta_tokens}"
218
224
  color_style = if delta_tokens > 10000
219
225
  :red
220
226
  elsif delta_tokens > 5000
@@ -222,7 +228,11 @@ module Clacky
222
228
  else
223
229
  :green
224
230
  end
225
- colored_delta = pastel.decorate(delta_str, color_style, :dim)
231
+ colored_delta = if delta_tokens.negative?
232
+ pastel.cyan(delta_str)
233
+ else
234
+ pastel.decorate(delta_str, color_style, :dim)
235
+ end
226
236
  token_info << colored_delta
227
237
 
228
238
  # Cache status indicator (using theme)
@@ -342,10 +352,13 @@ module Clacky
342
352
 
343
353
  # Remove <think>...</think> blocks (multiline, case-insensitive)
344
354
  # Also handles variations like <thinking>...</thinking>
345
- filtered = content.gsub(%r{<think(?:ing)?>\s*.*?\s*</think(?:ing)?>}mi, '')
355
+ filtered = content.gsub(%r{<think(?:ing)?>[\s\S]*?</think(?:ing)?>}mi, '')
346
356
 
347
- # Clean up extra whitespace left behind
348
- filtered.gsub(/\n{3,}/, "\n\n").strip
357
+ # Clean up multiple empty lines left behind (max 2 consecutive newlines)
358
+ filtered.gsub!(/\n{3,}/, "\n\n")
359
+
360
+ # Remove leading and trailing whitespace
361
+ filtered.strip
349
362
  end
350
363
 
351
364
  # Show tool call
@@ -380,7 +393,7 @@ module Clacky
380
393
  def show_complete(iterations:, cost:, duration: nil, cache_stats: nil)
381
394
  # Update status back to 'idle' when task is complete
382
395
  update_sessionbar(status: 'idle')
383
-
396
+
384
397
  # Clear user tip when agent stops working
385
398
  @input_area.clear_user_tip
386
399
  @layout.render_input
@@ -439,10 +452,10 @@ module Clacky
439
452
  def clear_progress
440
453
  # Calculate elapsed time before stopping
441
454
  elapsed_time = @progress_start_time ? (Time.now - @progress_start_time).to_i : 0
442
-
455
+
443
456
  # Stop the progress thread
444
457
  stop_progress_thread
445
-
458
+
446
459
  # Update the final progress line to gray (stopped state)
447
460
  if @progress_message && elapsed_time > 0
448
461
  final_output = @renderer.render_progress("#{@progress_message}… (#{elapsed_time}s)")
@@ -544,7 +557,7 @@ module Clacky
544
557
 
545
558
  # Create InlineInput with styled prompt
546
559
  inline_input = Components::InlineInput.new(
547
- prompt: " Press Enter to approve, 'n' to reject, or provide feedback: ",
560
+ prompt: "Press Enter/y to approve(Shift+Tab for all), 'n' to reject, or type feedback: ",
548
561
  default: nil
549
562
  )
550
563
  @inline_input = inline_input
@@ -556,8 +569,9 @@ module Clacky
556
569
  # Collect input (blocks until user presses Enter)
557
570
  result_text = inline_input.collect
558
571
 
559
- # Clean up - remove the inline input line (use layout to track position)
560
- @layout.remove_last_line
572
+ # Clean up - remove the inline input lines (handle wrapped lines)
573
+ line_count = inline_input.line_count
574
+ @layout.remove_last_line(line_count)
561
575
 
562
576
  # Append the final response to output
563
577
  if result_text.nil?
@@ -594,41 +608,15 @@ module Clacky
594
608
  require 'diffy'
595
609
 
596
610
  diff = Diffy::Diff.new(old_content, new_content, context: 3)
597
- all_lines = diff.to_s(:color).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
611
+ diff_lines = diff.to_s(:color).lines
626
612
 
627
- display_lines = numbered_lines.first(max_lines)
628
- display_lines.each { |line| append_output(line) }
613
+ # Show diff without line numbers
614
+ diff_lines.take(max_lines).each do |line|
615
+ append_output(line.chomp)
616
+ end
629
617
 
630
- if all_lines.size > max_lines
631
- append_output("\n... (#{all_lines.size - max_lines} more lines, diff truncated)")
618
+ if diff_lines.size > max_lines
619
+ append_output("\n... (#{diff_lines.size - max_lines} more lines, diff truncated)")
632
620
  end
633
621
  rescue LoadError
634
622
  # Fallback if diffy is not available
@@ -788,16 +776,19 @@ module Clacky
788
776
 
789
777
  # Handle key input for InlineInput
790
778
  def handle_inline_input_key(key)
779
+ # Get old line count BEFORE modification
780
+ old_line_count = @inline_input.line_count
781
+
791
782
  result = @inline_input.handle_key(key)
792
783
 
793
784
  case result[:action]
794
785
  when :update
795
- # Update the last line of output with current input (use layout to track position)
796
- @layout.update_last_line(@inline_input.render)
786
+ # Update the output area with current input (considering wrapped lines)
787
+ @layout.update_last_line(@inline_input.render, old_line_count)
797
788
  # Position cursor for inline input
798
789
  @layout.position_inline_input_cursor(@inline_input)
799
790
  when :submit, :cancel
800
- # InlineInput is done, will be cleaned up by request_confirmation
791
+ # InlineInput is done, will be cleaned up by request_confirmation after collect returns
801
792
  nil
802
793
  when :toggle_mode
803
794
  # Update mode and session bar info, but don't render yet
@@ -30,20 +30,18 @@ module Clacky
30
30
  /\.ini$/,
31
31
  /\.conf$/,
32
32
  /\.config$/,
33
- /config\//,
34
- /\.config\//
35
33
  ].freeze
36
34
 
37
35
  # Find .gitignore file in the search path or parent directories
38
36
  # Only searches within the search path and up to the current working directory
39
37
  def self.find_gitignore(path)
40
38
  search_path = File.directory?(path) ? path : File.dirname(path)
41
-
39
+
42
40
  # Look for .gitignore in current and parent directories
43
41
  current = File.expand_path(search_path)
44
42
  cwd = File.expand_path(Dir.pwd)
45
43
  root = File.expand_path('/')
46
-
44
+
47
45
  # Limit search: only go up to current working directory
48
46
  # This prevents finding .gitignore files from unrelated parent directories
49
47
  # when searching in temporary directories (like /tmp in tests)
@@ -52,16 +50,16 @@ module Clacky
52
50
  else
53
51
  current
54
52
  end
55
-
53
+
56
54
  loop do
57
55
  gitignore = File.join(current, '.gitignore')
58
56
  return gitignore if File.exist?(gitignore)
59
-
57
+
60
58
  # Stop if we've reached the search limit or root
61
59
  break if current == search_limit || current == root
62
60
  current = File.dirname(current)
63
61
  end
64
-
62
+
65
63
  nil
66
64
  end
67
65
 
@@ -71,10 +69,10 @@ module Clacky
71
69
  # Expand both paths to handle symlinks and relative paths correctly
72
70
  expanded_file = File.expand_path(file)
73
71
  expanded_base = File.expand_path(base_path)
74
-
72
+
75
73
  # For files, use the directory as base
76
74
  expanded_base = File.dirname(expanded_base) if File.file?(expanded_base)
77
-
75
+
78
76
  # Calculate relative path
79
77
  if expanded_file.start_with?(expanded_base)
80
78
  relative_path = expanded_file[(expanded_base.length + 1)..-1] || File.basename(expanded_file)
@@ -82,10 +80,10 @@ module Clacky
82
80
  # File is outside base path - use just the filename
83
81
  relative_path = File.basename(expanded_file)
84
82
  end
85
-
83
+
86
84
  # Clean up relative path
87
85
  relative_path = relative_path.sub(/^\.\//, '') if relative_path
88
-
86
+
89
87
  if gitignore
90
88
  # Use .gitignore rules
91
89
  gitignore.ignored?(relative_path)
@@ -96,7 +94,7 @@ module Clacky
96
94
  File.fnmatch(pattern, relative_path, File::FNM_PATHNAME | File::FNM_DOTMATCH)
97
95
  else
98
96
  # Match pattern as a path component (not substring of absolute path)
99
- relative_path.start_with?("#{pattern}/") ||
97
+ relative_path.start_with?("#{pattern}/") ||
100
98
  relative_path.include?("/#{pattern}/") ||
101
99
  relative_path == pattern ||
102
100
  File.basename(relative_path) == pattern
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "0.6.2"
4
+ VERSION = "0.6.4"
5
5
  end
data/lib/clacky.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  require_relative "clacky/version"
4
4
  require_relative "clacky/config"
5
5
  require_relative "clacky/client"
6
+ require_relative "clacky/skill"
7
+ require_relative "clacky/skill_loader"
6
8
 
7
9
  # Agent system
8
10
  require_relative "clacky/model_pricing"
@@ -35,6 +37,7 @@ require_relative "clacky/agent"
35
37
  require_relative "clacky/cli"
36
38
 
37
39
  module Clacky
38
- class Error < StandardError; end
40
+ class AgentError < StandardError; end
39
41
  class AgentInterrupted < StandardError; end
42
+ class ToolCallError < AgentError; end # Raised when tool call fails due to invalid parameters
40
43
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: openclacky
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.2
4
+ version: 0.6.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - windy
@@ -159,6 +159,7 @@ files:
159
159
  - clacky-legacy/clacky.gemspec
160
160
  - clacky-legacy/clarky.gemspec
161
161
  - docs/ui2-architecture.md
162
+ - docs/why-openclacky.md
162
163
  - homebrew/README.md
163
164
  - homebrew/openclacky.rb
164
165
  - lib/clacky.rb
@@ -167,11 +168,14 @@ files:
167
168
  - lib/clacky/cli.rb
168
169
  - lib/clacky/client.rb
169
170
  - lib/clacky/config.rb
171
+ - lib/clacky/default_skills/skill-add/SKILL.md
170
172
  - lib/clacky/gitignore_parser.rb
171
173
  - lib/clacky/hook_manager.rb
172
174
  - lib/clacky/model_pricing.rb
173
175
  - lib/clacky/progress_indicator.rb
174
176
  - lib/clacky/session_manager.rb
177
+ - lib/clacky/skill.rb
178
+ - lib/clacky/skill_loader.rb
175
179
  - lib/clacky/thinking_verbs.rb
176
180
  - lib/clacky/tool_registry.rb
177
181
  - lib/clacky/tools/base.rb
@@ -191,6 +195,7 @@ files:
191
195
  - lib/clacky/ui2.rb
192
196
  - lib/clacky/ui2/README.md
193
197
  - lib/clacky/ui2/components/base_component.rb
198
+ - lib/clacky/ui2/components/command_suggestions.rb
194
199
  - lib/clacky/ui2/components/common_component.rb
195
200
  - lib/clacky/ui2/components/inline_input.rb
196
201
  - lib/clacky/ui2/components/input_area.rb