openclacky 0.5.4 → 0.5.5
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/lib/clacky/agent.rb +34 -16
- data/lib/clacky/cli.rb +48 -131
- data/lib/clacky/client.rb +53 -7
- data/lib/clacky/model_pricing.rb +280 -0
- data/lib/clacky/tools/grep.rb +30 -7
- data/lib/clacky/tools/safe_shell.rb +9 -4
- data/lib/clacky/tools/shell.rb +60 -22
- data/lib/clacky/ui/banner.rb +22 -11
- data/lib/clacky/ui/enhanced_prompt.rb +61 -164
- data/lib/clacky/ui/statusbar.rb +0 -2
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +1 -1
- metadata +2 -2
- data/lib/clacky/conversation.rb +0 -41
|
@@ -28,9 +28,9 @@ module Clacky
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
# Read user input with enhanced features
|
|
31
|
-
# @param prefix [String] Prompt prefix (default: "
|
|
31
|
+
# @param prefix [String] Prompt prefix (default: "❯")
|
|
32
32
|
# @return [Hash, nil] { text: String, images: Array } or nil on EOF
|
|
33
|
-
def read_input(prefix: "
|
|
33
|
+
def read_input(prefix: "❯")
|
|
34
34
|
@images = []
|
|
35
35
|
lines = []
|
|
36
36
|
cursor_pos = 0
|
|
@@ -38,8 +38,8 @@ module Clacky
|
|
|
38
38
|
@last_ctrl_c_time = nil # Track when Ctrl+C was last pressed
|
|
39
39
|
|
|
40
40
|
loop do
|
|
41
|
-
# Display the prompt
|
|
42
|
-
|
|
41
|
+
# Display the prompt (simplified version)
|
|
42
|
+
display_simple_prompt(lines, prefix, line_index, cursor_pos)
|
|
43
43
|
|
|
44
44
|
# Read a single character/key
|
|
45
45
|
begin
|
|
@@ -93,7 +93,7 @@ module Clacky
|
|
|
93
93
|
when "\r" # Enter - submit
|
|
94
94
|
# Submit if not empty
|
|
95
95
|
unless lines.join.strip.empty? && @images.empty?
|
|
96
|
-
|
|
96
|
+
clear_simple_prompt(lines.size)
|
|
97
97
|
# Replace placeholders with actual pasted content
|
|
98
98
|
final_text = expand_placeholders(lines.join("\n"))
|
|
99
99
|
return { text: final_text, images: @images.dup }
|
|
@@ -110,7 +110,7 @@ module Clacky
|
|
|
110
110
|
|
|
111
111
|
if time_since_last < 2.0 # Within 2 seconds of last Ctrl+C
|
|
112
112
|
# Second Ctrl+C within 2 seconds - exit
|
|
113
|
-
|
|
113
|
+
clear_simple_prompt(lines.size)
|
|
114
114
|
return nil
|
|
115
115
|
else
|
|
116
116
|
# First Ctrl+C - clear content
|
|
@@ -124,7 +124,7 @@ module Clacky
|
|
|
124
124
|
end
|
|
125
125
|
else
|
|
126
126
|
# Input is empty - exit immediately
|
|
127
|
-
|
|
127
|
+
clear_simple_prompt(lines.size)
|
|
128
128
|
return nil
|
|
129
129
|
end
|
|
130
130
|
|
|
@@ -226,22 +226,10 @@ module Clacky
|
|
|
226
226
|
|
|
227
227
|
private
|
|
228
228
|
|
|
229
|
-
#
|
|
230
|
-
def
|
|
231
|
-
result = text.dup
|
|
232
|
-
@paste_placeholders.each do |placeholder, actual_content|
|
|
233
|
-
result.gsub!(placeholder, actual_content)
|
|
234
|
-
end
|
|
235
|
-
result
|
|
236
|
-
end
|
|
237
|
-
|
|
238
|
-
# Display the prompt box with images and input
|
|
239
|
-
def display_prompt_box(lines, prefix, line_index, cursor_pos)
|
|
240
|
-
width = TTY::Screen.width - 4 # Use full terminal width (minus 4 for borders)
|
|
241
|
-
|
|
229
|
+
# Display simplified prompt (just prefix and input, no box)
|
|
230
|
+
def display_simple_prompt(lines, prefix, line_index, cursor_pos)
|
|
242
231
|
# Clear previous display if exists
|
|
243
232
|
if @last_display_lines && @last_display_lines > 0
|
|
244
|
-
# Move cursor up and clear each line
|
|
245
233
|
@last_display_lines.times do
|
|
246
234
|
print "\e[1A" # Move up one line
|
|
247
235
|
print "\e[2K" # Clear entire line
|
|
@@ -250,172 +238,72 @@ module Clacky
|
|
|
250
238
|
end
|
|
251
239
|
|
|
252
240
|
lines_to_display = []
|
|
241
|
+
|
|
242
|
+
# Get terminal width for full-width separator
|
|
243
|
+
term_width = TTY::Screen.width
|
|
253
244
|
|
|
254
|
-
#
|
|
245
|
+
# Top separator line (full width)
|
|
246
|
+
lines_to_display << @pastel.dim("─" * term_width)
|
|
247
|
+
|
|
248
|
+
# Show images if any
|
|
255
249
|
if @images.any?
|
|
256
|
-
lines_to_display << @pastel.dim("╭─ Attached Images " + "─" * (width - 19) + "╮")
|
|
257
250
|
@images.each_with_index do |img_path, idx|
|
|
258
251
|
filename = File.basename(img_path)
|
|
259
|
-
# Check if file exists before getting size
|
|
260
252
|
filesize = File.exist?(img_path) ? format_filesize(File.size(img_path)) : "N/A"
|
|
261
|
-
|
|
262
|
-
display_content = line_content.ljust(width - 2)
|
|
263
|
-
lines_to_display << @pastel.dim("│ ") + display_content + @pastel.dim(" │")
|
|
253
|
+
lines_to_display << @pastel.dim("[Image #{idx + 1}] #{filename} (#{filesize})")
|
|
264
254
|
end
|
|
265
|
-
lines_to_display << @pastel.dim("╰" + "─" * width + "╯")
|
|
266
255
|
lines_to_display << ""
|
|
267
256
|
end
|
|
268
257
|
|
|
269
|
-
# Display input
|
|
270
|
-
hint = "Shift+Enter:newline | Enter:submit | Ctrl+C:cancel"
|
|
271
|
-
lines_to_display << @pastel.dim("╭─ Message " + "─" * (width - 10) + "╮")
|
|
272
|
-
hint_line = @pastel.dim(hint)
|
|
273
|
-
padding = " " * [(width - hint.length - 2), 0].max
|
|
274
|
-
lines_to_display << @pastel.dim("│ ") + hint_line + padding + @pastel.dim(" │")
|
|
275
|
-
lines_to_display << @pastel.dim("├" + "─" * width + "┤")
|
|
276
|
-
|
|
277
|
-
# Display input lines with word wrap
|
|
258
|
+
# Display input lines
|
|
278
259
|
display_lines = lines.empty? ? [""] : lines
|
|
279
|
-
max_display_lines = 15 # Show up to 15 wrapped lines
|
|
280
|
-
|
|
281
|
-
# Flatten all lines with word wrap
|
|
282
|
-
wrapped_display_lines = []
|
|
283
|
-
line_to_wrapped_mapping = [] # Track which original line each wrapped line belongs to
|
|
284
260
|
|
|
285
|
-
display_lines.each_with_index do |line,
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
original_line: original_idx,
|
|
300
|
-
start_pos: start_pos
|
|
301
|
-
}
|
|
302
|
-
start_pos += content_width
|
|
303
|
-
end
|
|
304
|
-
end
|
|
305
|
-
end
|
|
306
|
-
|
|
307
|
-
# Find which wrapped line contains the cursor
|
|
308
|
-
cursor_wrapped_line_idx = 0
|
|
309
|
-
cursor_in_wrapped_pos = cursor_pos
|
|
310
|
-
content_width = width - 2
|
|
311
|
-
|
|
312
|
-
# Find all wrapped lines for the current line_index
|
|
313
|
-
current_line_wrapped = wrapped_display_lines.select.with_index { |wl, idx| wl[:original_line] == line_index }
|
|
314
|
-
|
|
315
|
-
if current_line_wrapped.any?
|
|
316
|
-
# Iterate through wrapped lines to find where cursor belongs
|
|
317
|
-
accumulated_chars = 0
|
|
318
|
-
found = false
|
|
319
|
-
|
|
320
|
-
current_line_wrapped.each_with_index do |wrapped_line, local_idx|
|
|
321
|
-
line_start = wrapped_line[:start_pos]
|
|
322
|
-
line_length = wrapped_line[:text].chars.length
|
|
323
|
-
line_end = line_start + line_length
|
|
324
|
-
|
|
325
|
-
# Find global index of this wrapped line
|
|
326
|
-
global_idx = wrapped_display_lines.index { |wl| wl == wrapped_line }
|
|
327
|
-
|
|
328
|
-
if cursor_pos >= line_start && cursor_pos < line_end
|
|
329
|
-
# Cursor is within this wrapped line
|
|
330
|
-
cursor_wrapped_line_idx = global_idx
|
|
331
|
-
cursor_in_wrapped_pos = cursor_pos - line_start
|
|
332
|
-
found = true
|
|
333
|
-
break
|
|
334
|
-
elsif cursor_pos == line_end && local_idx == current_line_wrapped.length - 1
|
|
335
|
-
# Cursor is at the very end of the last wrapped line for this line_index
|
|
336
|
-
cursor_wrapped_line_idx = global_idx
|
|
337
|
-
cursor_in_wrapped_pos = line_length
|
|
338
|
-
found = true
|
|
339
|
-
break
|
|
261
|
+
display_lines.each_with_index do |line, idx|
|
|
262
|
+
if idx == 0
|
|
263
|
+
# First line with prefix
|
|
264
|
+
if idx == line_index
|
|
265
|
+
# Show cursor on this line
|
|
266
|
+
chars = line.chars
|
|
267
|
+
before_cursor = chars[0...cursor_pos].join
|
|
268
|
+
cursor_char = chars[cursor_pos] || " "
|
|
269
|
+
after_cursor = chars[(cursor_pos + 1)..-1]&.join || ""
|
|
270
|
+
|
|
271
|
+
line_display = "#{prefix} #{before_cursor}#{@pastel.on_white(@pastel.black(cursor_char))}#{after_cursor}"
|
|
272
|
+
lines_to_display << line_display
|
|
273
|
+
else
|
|
274
|
+
lines_to_display << "#{prefix} #{line}"
|
|
340
275
|
end
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
# Fallback: if not found, place cursor at the end of the last wrapped line
|
|
344
|
-
unless found
|
|
345
|
-
last_wrapped = current_line_wrapped.last
|
|
346
|
-
cursor_wrapped_line_idx = wrapped_display_lines.index { |wl| wl == last_wrapped }
|
|
347
|
-
cursor_in_wrapped_pos = last_wrapped[:text].chars.length
|
|
348
|
-
end
|
|
349
|
-
end
|
|
350
|
-
|
|
351
|
-
# Determine which wrapped lines to display (centered around cursor)
|
|
352
|
-
if wrapped_display_lines.size <= max_display_lines
|
|
353
|
-
display_start = 0
|
|
354
|
-
display_end = wrapped_display_lines.size - 1
|
|
355
|
-
else
|
|
356
|
-
# Center view around cursor line
|
|
357
|
-
half_display = max_display_lines / 2
|
|
358
|
-
display_start = [cursor_wrapped_line_idx - half_display, 0].max
|
|
359
|
-
display_end = [display_start + max_display_lines - 1, wrapped_display_lines.size - 1].min
|
|
360
|
-
|
|
361
|
-
# Adjust if we're near the end
|
|
362
|
-
if display_end - display_start < max_display_lines - 1
|
|
363
|
-
display_start = [display_end - max_display_lines + 1, 0].max
|
|
364
|
-
end
|
|
365
|
-
end
|
|
366
|
-
|
|
367
|
-
# Display the wrapped lines
|
|
368
|
-
(display_start..display_end).each do |idx|
|
|
369
|
-
wrapped_line = wrapped_display_lines[idx]
|
|
370
|
-
line_text = wrapped_line[:text]
|
|
371
|
-
line_chars = line_text.chars
|
|
372
|
-
content_width = width - 2
|
|
373
|
-
|
|
374
|
-
# Pad to full width
|
|
375
|
-
display_line = line_text.ljust(content_width)
|
|
376
|
-
|
|
377
|
-
if idx == cursor_wrapped_line_idx
|
|
378
|
-
# Show cursor on this wrapped line
|
|
379
|
-
before_cursor = line_chars[0...cursor_in_wrapped_pos].join
|
|
380
|
-
cursor_char = line_chars[cursor_in_wrapped_pos] || " "
|
|
381
|
-
after_cursor_chars = line_chars[(cursor_in_wrapped_pos + 1)..-1]
|
|
382
|
-
after_cursor = after_cursor_chars ? after_cursor_chars.join : ""
|
|
383
|
-
|
|
384
|
-
# Calculate padding
|
|
385
|
-
content_length = before_cursor.length + 1 + after_cursor.length
|
|
386
|
-
padding = " " * [content_width - content_length, 0].max
|
|
387
|
-
|
|
388
|
-
line_display = before_cursor + @pastel.on_white(@pastel.black(cursor_char)) + after_cursor + padding
|
|
389
|
-
lines_to_display << @pastel.dim("│ ") + line_display + @pastel.dim(" │")
|
|
390
276
|
else
|
|
391
|
-
|
|
277
|
+
# Continuation lines (indented to align with first line content)
|
|
278
|
+
indent = " " * (prefix.length + 1)
|
|
279
|
+
if idx == line_index
|
|
280
|
+
# Show cursor on this line
|
|
281
|
+
chars = line.chars
|
|
282
|
+
before_cursor = chars[0...cursor_pos].join
|
|
283
|
+
cursor_char = chars[cursor_pos] || " "
|
|
284
|
+
after_cursor = chars[(cursor_pos + 1)..-1]&.join || ""
|
|
285
|
+
|
|
286
|
+
line_display = "#{indent}#{before_cursor}#{@pastel.on_white(@pastel.black(cursor_char))}#{after_cursor}"
|
|
287
|
+
lines_to_display << line_display
|
|
288
|
+
else
|
|
289
|
+
lines_to_display << "#{indent}#{line}"
|
|
290
|
+
end
|
|
392
291
|
end
|
|
393
292
|
end
|
|
394
293
|
|
|
395
|
-
#
|
|
396
|
-
|
|
397
|
-
scroll_info = " (#{display_start + 1}-#{display_end + 1}/#{wrapped_display_lines.size} lines) "
|
|
398
|
-
lines_to_display << @pastel.dim("│#{scroll_info.center(width)}│")
|
|
399
|
-
end
|
|
294
|
+
# Bottom separator line (full width)
|
|
295
|
+
lines_to_display << @pastel.dim("─" * term_width)
|
|
400
296
|
|
|
401
|
-
#
|
|
402
|
-
footer_text = "Line #{line_index + 1}/#{display_lines.size} | Char #{cursor_pos}/#{(display_lines[line_index] || "").chars.length}"
|
|
403
|
-
# Total width = "╰─ " (3) + footer_text + " ─...─╯" (width - 3 - footer_text.length)
|
|
404
|
-
remaining_width = width - footer_text.length - 3 # 3 = "╰─ " length
|
|
405
|
-
footer_line = @pastel.dim("╰─ ") + @pastel.dim(footer_text) + @pastel.dim(" ") + @pastel.dim("─" * [remaining_width - 1, 0].max) + @pastel.dim("╯")
|
|
406
|
-
lines_to_display << footer_line
|
|
407
|
-
|
|
408
|
-
# Output all lines at once (use print to avoid extra newline at the end)
|
|
297
|
+
# Output all lines
|
|
409
298
|
print lines_to_display.join("\n")
|
|
410
|
-
print "\n"
|
|
299
|
+
print "\n"
|
|
411
300
|
|
|
412
301
|
# Remember how many lines we displayed
|
|
413
302
|
@last_display_lines = lines_to_display.size
|
|
414
303
|
end
|
|
415
304
|
|
|
416
|
-
# Clear prompt display
|
|
417
|
-
def
|
|
418
|
-
# Clear the prompt box we just displayed
|
|
305
|
+
# Clear simple prompt display
|
|
306
|
+
def clear_simple_prompt(num_lines)
|
|
419
307
|
if @last_display_lines && @last_display_lines > 0
|
|
420
308
|
@last_display_lines.times do
|
|
421
309
|
print "\e[1A" # Move up one line
|
|
@@ -425,6 +313,15 @@ module Clacky
|
|
|
425
313
|
end
|
|
426
314
|
end
|
|
427
315
|
|
|
316
|
+
# Expand placeholders to actual pasted content
|
|
317
|
+
def expand_placeholders(text)
|
|
318
|
+
result = text.dup
|
|
319
|
+
@paste_placeholders.each do |placeholder, actual_content|
|
|
320
|
+
result.gsub!(placeholder, actual_content)
|
|
321
|
+
end
|
|
322
|
+
result
|
|
323
|
+
end
|
|
324
|
+
|
|
428
325
|
# Read a single key press with escape sequence handling
|
|
429
326
|
# Handles UTF-8 multi-byte characters correctly
|
|
430
327
|
# Also detects rapid input (paste-like behavior)
|
data/lib/clacky/ui/statusbar.rb
CHANGED
|
@@ -45,14 +45,12 @@ module Clacky
|
|
|
45
45
|
status_line = " " + parts.join(separator)
|
|
46
46
|
|
|
47
47
|
puts status_line
|
|
48
|
-
puts @pastel.dim("─" * [TTY::Screen.width, 80].min)
|
|
49
48
|
end
|
|
50
49
|
|
|
51
50
|
# Display minimal status for non-interactive mode
|
|
52
51
|
def display_minimal(working_dir:, mode:)
|
|
53
52
|
dir_display = shorten_path(working_dir)
|
|
54
53
|
puts " #{@pastel.bright_cyan(dir_display)} #{@pastel.dim('│')} #{@pastel.yellow(mode)}"
|
|
55
|
-
puts @pastel.dim("─" * [TTY::Screen.width, 80].min)
|
|
56
54
|
end
|
|
57
55
|
|
|
58
56
|
private
|
data/lib/clacky/version.rb
CHANGED
data/lib/clacky.rb
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
require_relative "clacky/version"
|
|
4
4
|
require_relative "clacky/config"
|
|
5
5
|
require_relative "clacky/client"
|
|
6
|
-
require_relative "clacky/conversation"
|
|
7
6
|
|
|
8
7
|
# Agent system
|
|
8
|
+
require_relative "clacky/model_pricing"
|
|
9
9
|
require_relative "clacky/agent_config"
|
|
10
10
|
require_relative "clacky/hook_manager"
|
|
11
11
|
require_relative "clacky/tool_registry"
|
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.5.
|
|
4
|
+
version: 0.5.5
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- windy
|
|
@@ -136,9 +136,9 @@ files:
|
|
|
136
136
|
- lib/clacky/cli.rb
|
|
137
137
|
- lib/clacky/client.rb
|
|
138
138
|
- lib/clacky/config.rb
|
|
139
|
-
- lib/clacky/conversation.rb
|
|
140
139
|
- lib/clacky/gitignore_parser.rb
|
|
141
140
|
- lib/clacky/hook_manager.rb
|
|
141
|
+
- lib/clacky/model_pricing.rb
|
|
142
142
|
- lib/clacky/progress_indicator.rb
|
|
143
143
|
- lib/clacky/session_manager.rb
|
|
144
144
|
- lib/clacky/thinking_verbs.rb
|
data/lib/clacky/conversation.rb
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Clacky
|
|
4
|
-
class Conversation
|
|
5
|
-
attr_reader :messages
|
|
6
|
-
|
|
7
|
-
def initialize(api_key, model:, base_url:, max_tokens:)
|
|
8
|
-
@client = Client.new(api_key, base_url: base_url)
|
|
9
|
-
@model = model
|
|
10
|
-
@max_tokens = max_tokens
|
|
11
|
-
@messages = []
|
|
12
|
-
end
|
|
13
|
-
|
|
14
|
-
def send_message(content)
|
|
15
|
-
# Add user message to history
|
|
16
|
-
@messages << {
|
|
17
|
-
role: "user",
|
|
18
|
-
content: content
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
# Get response from Claude
|
|
22
|
-
response_text = @client.send_messages(@messages, model: @model, max_tokens: @max_tokens)
|
|
23
|
-
|
|
24
|
-
# Add assistant response to history
|
|
25
|
-
@messages << {
|
|
26
|
-
role: "assistant",
|
|
27
|
-
content: response_text
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
response_text
|
|
31
|
-
end
|
|
32
|
-
|
|
33
|
-
def clear
|
|
34
|
-
@messages = []
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
def history
|
|
38
|
-
@messages.dup
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
end
|