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.
@@ -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: "You:")
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: "You:")
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 box
42
- display_prompt_box(lines, prefix, line_index, cursor_pos)
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
- clear_prompt_display(lines.size)
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
- clear_prompt_display(lines.size)
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
- clear_prompt_display(lines.size)
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
- # Expand placeholders to actual pasted content
230
- def expand_placeholders(text)
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
- # Display images if any
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
- line_content = " #{idx + 1}. #{filename} (#{filesize})"
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 box
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, original_idx|
286
- line_chars = line.chars
287
- content_width = width - 2 # Available width for content (excluding borders)
288
-
289
- if line_chars.length <= content_width
290
- # Line fits in one display line
291
- wrapped_display_lines << { text: line, original_line: original_idx, start_pos: 0 }
292
- else
293
- # Line needs wrapping
294
- start_pos = 0
295
- while start_pos < line_chars.length
296
- chunk_chars = line_chars[start_pos...[start_pos + content_width, line_chars.length].min]
297
- wrapped_display_lines << {
298
- text: chunk_chars.join,
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
- lines_to_display << @pastel.dim("│ ") + display_line + @pastel.dim(" │")
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
- # Show scroll indicator if needed
396
- if wrapped_display_lines.size > max_display_lines
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
- # Footer - calculate width properly
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" # Add one controlled newline
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 after submission
417
- def clear_prompt_display(num_lines)
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)
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Clacky
4
- VERSION = "0.5.4"
4
+ VERSION = "0.5.5"
5
5
  end
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
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
@@ -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