openclacky 0.7.0 → 0.7.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.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.clacky/skills/commit/SKILL.md +29 -4
  3. data/.clackyrules +3 -1
  4. data/CHANGELOG.md +103 -2
  5. data/README.md +70 -161
  6. data/bin/clarky +11 -0
  7. data/docs/HOW-TO-USE-CN.md +96 -0
  8. data/docs/HOW-TO-USE.md +94 -0
  9. data/docs/config.example.yml +27 -0
  10. data/docs/deploy_subagent_design.md +540 -0
  11. data/docs/time_machine_design.md +247 -0
  12. data/docs/why-openclacky.md +0 -1
  13. data/lib/clacky/agent/cost_tracker.rb +180 -0
  14. data/lib/clacky/agent/llm_caller.rb +54 -0
  15. data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
  16. data/lib/clacky/agent/message_compressor_helper.rb +534 -0
  17. data/lib/clacky/agent/session_serializer.rb +152 -0
  18. data/lib/clacky/agent/skill_manager.rb +138 -0
  19. data/lib/clacky/agent/system_prompt_builder.rb +96 -0
  20. data/lib/clacky/agent/time_machine.rb +199 -0
  21. data/lib/clacky/agent/tool_executor.rb +434 -0
  22. data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
  23. data/lib/clacky/agent.rb +260 -1370
  24. data/lib/clacky/agent_config.rb +447 -10
  25. data/lib/clacky/cli.rb +275 -98
  26. data/lib/clacky/client.rb +12 -2
  27. data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
  28. data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
  29. data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
  30. data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
  31. data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
  32. data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
  33. data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
  34. data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
  35. data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
  36. data/lib/clacky/default_skills/new/SKILL.md +2 -2
  37. data/lib/clacky/json_ui_controller.rb +195 -0
  38. data/lib/clacky/providers.rb +107 -0
  39. data/lib/clacky/skill.rb +48 -7
  40. data/lib/clacky/skill_loader.rb +7 -0
  41. data/lib/clacky/tools/edit.rb +105 -48
  42. data/lib/clacky/tools/file_reader.rb +44 -73
  43. data/lib/clacky/tools/invoke_skill.rb +89 -0
  44. data/lib/clacky/tools/list_tasks.rb +54 -0
  45. data/lib/clacky/tools/redo_task.rb +41 -0
  46. data/lib/clacky/tools/safe_shell.rb +1 -1
  47. data/lib/clacky/tools/shell.rb +74 -62
  48. data/lib/clacky/tools/trash_manager.rb +1 -1
  49. data/lib/clacky/tools/undo_task.rb +32 -0
  50. data/lib/clacky/tools/web_fetch.rb +2 -1
  51. data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
  52. data/lib/clacky/ui2/components/inline_input.rb +23 -2
  53. data/lib/clacky/ui2/components/input_area.rb +65 -21
  54. data/lib/clacky/ui2/components/modal_component.rb +199 -62
  55. data/lib/clacky/ui2/layout_manager.rb +75 -25
  56. data/lib/clacky/ui2/line_editor.rb +23 -2
  57. data/lib/clacky/ui2/markdown_renderer.rb +31 -10
  58. data/lib/clacky/ui2/screen_buffer.rb +2 -0
  59. data/lib/clacky/ui2/ui_controller.rb +316 -37
  60. data/lib/clacky/ui2.rb +2 -0
  61. data/lib/clacky/ui_interface.rb +50 -0
  62. data/lib/clacky/utils/arguments_parser.rb +31 -3
  63. data/lib/clacky/utils/file_processor.rb +13 -18
  64. data/lib/clacky/version.rb +1 -1
  65. data/lib/clacky.rb +19 -9
  66. data/scripts/install.sh +274 -97
  67. data/scripts/uninstall.sh +12 -12
  68. metadata +40 -13
  69. data/.clacky/skills/test-skill/SKILL.md +0 -15
  70. data/lib/clacky/compression/base.rb +0 -231
  71. data/lib/clacky/compression/standard.rb +0 -339
  72. data/lib/clacky/config.rb +0 -117
  73. /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
  74. /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
  75. /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
  76. /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
  77. /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
  78. /data/lib/clacky/{trash_directory.rb → utils/trash_directory.rb} +0 -0
@@ -17,20 +17,39 @@ module Clacky
17
17
  @height = 16
18
18
  @title = ""
19
19
  @fields = []
20
+ @choices = []
20
21
  @values = {}
22
+ @selected_index = 0
23
+ @mode = :form
21
24
  end
22
25
 
23
26
  # Configure and show the modal
24
27
  # @param title [String] Modal title
25
- # @param fields [Array<Hash>] Field definitions
28
+ # @param fields [Array<Hash>] Field definitions (for form mode)
29
+ # @param choices [Array<Hash>] Choice definitions (for menu mode)
30
+ # Example: [{ name: "Option 1", value: :opt1 }, { name: "---", disabled: true }]
26
31
  # @param validator [Proc, nil] Optional validation callback that receives values hash
27
32
  # Should return { success: true } or { success: false, error: "message" }
28
- # @return [Hash, nil] Hash of field values, or nil if cancelled
29
- def show(title:, fields:, validator: nil)
33
+ # @return [Hash, nil] Hash of field values or selected value, or nil if cancelled
34
+ def show(title:, fields: nil, choices: nil, validator: nil)
30
35
  @title = title
31
- @fields = fields
36
+ @mode = choices ? :menu : :form
37
+ @fields = fields || []
38
+ @choices = choices || []
32
39
  @values = {}
33
40
  @error_message = nil
41
+ @selected_index = 0
42
+
43
+ # For menu mode, find first non-disabled choice
44
+ if @mode == :menu
45
+ @selected_index = @choices.index { |c| !c[:disabled] } || 0
46
+ end
47
+
48
+ # Adjust height for menu mode
49
+ if @mode == :menu
50
+ visible_items = [@choices.length, 15].min
51
+ @height = visible_items + 4 # +4 for title, borders, and instructions
52
+ end
34
53
 
35
54
  # Get terminal size
36
55
  term_height, term_width = IO.console.winsize
@@ -40,60 +59,12 @@ module Clacky
40
59
  start_col = [(term_width - @width) / 2, 1].max
41
60
 
42
61
  begin
43
- loop do
44
- # Draw modal background and border
45
- draw_modal(start_row, start_col)
46
-
47
- # Draw error message if present
48
- if @error_message
49
- draw_error_message(start_row + @height - 5, start_col)
50
- end
51
-
52
- # Draw instructions
53
- draw_buttons(start_row + @height - 3, start_col)
54
-
55
- # Collect input for each field
56
- current_row = start_row + 3
57
- @fields.each do |field|
58
- value = collect_field_input(field, current_row, start_col)
59
- if value == :cancelled
60
- print "\e[?25l" # Hide cursor
61
- return nil # User pressed Esc
62
- end
63
- @values[field[:name]] = value
64
- current_row += 2
65
- end
66
-
67
- # All fields collected - validate if validator provided
68
- if validator
69
- # Show "Testing..." message
70
- testing_row = start_row + @height - 5
71
- testing_col = start_col + 3
72
- print "\e[#{testing_row};#{testing_col}H\e[K"
73
- print @pastel.cyan("⏳ Testing connection...")
74
- STDOUT.flush
75
-
76
- validation_result = validator.call(@values)
77
-
78
- # Clear testing message
79
- print "\e[#{testing_row};#{testing_col}H\e[K"
80
-
81
- if validation_result[:success]
82
- # Validation passed - hide cursor and return values
83
- print "\e[?25l"
84
- return @values
85
- else
86
- # Validation failed - show error and loop again
87
- @error_message = validation_result[:error] || "Validation failed"
88
- # Clear modal to redraw with error
89
- clear_modal(start_row, start_col)
90
- sleep 1.5 # Give user time to read the error
91
- end
92
- else
93
- # No validator - return immediately
94
- print "\e[?25l"
95
- return @values
96
- end
62
+ if @mode == :menu
63
+ # Menu mode - show choices and handle selection
64
+ return show_menu_mode(start_row, start_col)
65
+ else
66
+ # Form mode - collect field inputs
67
+ return show_form_mode(start_row, start_col, validator)
97
68
  end
98
69
  ensure
99
70
  # Clear modal area
@@ -109,6 +80,101 @@ module Clacky
109
80
 
110
81
  private
111
82
 
83
+ # Show menu mode
84
+ private def show_menu_mode(start_row, start_col)
85
+ loop do
86
+ # Draw menu
87
+ draw_modal(start_row, start_col)
88
+ draw_menu_choices(start_row + 2, start_col)
89
+ draw_menu_instructions(start_row + @height - 2, start_col)
90
+
91
+ # Read input
92
+ char = STDIN.getch
93
+
94
+ case char
95
+ when "\r", "\n" # Enter - select current choice
96
+ selected = @choices[@selected_index]
97
+ return nil if selected.nil? || selected[:disabled]
98
+ print "\e[?25l" # Hide cursor
99
+ return selected[:value]
100
+ when "\e" # Escape sequence
101
+ seq = STDIN.read_nonblock(2) rescue ''
102
+ if seq.empty?
103
+ # Just Esc key - cancel
104
+ print "\e[?25l"
105
+ return nil
106
+ elsif seq == '[A' # Up arrow
107
+ move_menu_selection(-1)
108
+ elsif seq == '[B' # Down arrow
109
+ move_menu_selection(1)
110
+ end
111
+ when "\u0003" # Ctrl+C
112
+ print "\e[?25l"
113
+ return nil
114
+ when 'k', 'K' # Vim-style up
115
+ move_menu_selection(-1)
116
+ when 'j', 'J' # Vim-style down
117
+ move_menu_selection(1)
118
+ when 'q', 'Q' # Quit
119
+ print "\e[?25l"
120
+ return nil
121
+ end
122
+ end
123
+ end
124
+
125
+ # Show form mode
126
+ private def show_form_mode(start_row, start_col, validator)
127
+ loop do
128
+ # Draw modal background and border
129
+ draw_modal(start_row, start_col)
130
+
131
+ # Draw error message if present
132
+ if @error_message
133
+ draw_error_message(start_row + @height - 5, start_col)
134
+ end
135
+
136
+ # Draw instructions
137
+ draw_buttons(start_row + @height - 3, start_col)
138
+
139
+ # Collect input for each field
140
+ current_row = start_row + 3
141
+ @fields.each do |field|
142
+ # Use previously entered value as default if validation failed
143
+ field_with_previous = field.dup
144
+ field_with_previous[:default] = @values[field[:name]] || field[:default]
145
+
146
+ value = collect_field_input(field_with_previous, current_row, start_col)
147
+ if value == :cancelled
148
+ print "\e[?25l" # Hide cursor
149
+ return nil # User pressed Esc
150
+ end
151
+ @values[field[:name]] = value
152
+ current_row += 2
153
+ end
154
+
155
+ # All fields collected - validate if validator provided
156
+ if validator
157
+ # Show "Testing..." message
158
+ testing_row = start_row + @height - 5
159
+ testing_col = start_col + 3
160
+ print "\e[#{testing_row};#{testing_col}H\e[K"
161
+ print "\e[#{testing_row + 1};#{testing_col}H\e[K"
162
+ print @pastel.cyan("⏳ Testing connection...")
163
+ STDOUT.flush
164
+
165
+ validation_result = validator.call(@values)
166
+
167
+ # Clear testing messages
168
+ print "\e[#{testing_row};#{testing_col}H\e[K"
169
+ print "\e[#{testing_row + 1};#{testing_col}H\e[K"
170
+ else
171
+ # No validator - return immediately
172
+ print "\e[?25l"
173
+ return @values
174
+ end
175
+ end
176
+ end
177
+
112
178
  # Draw the modal background and border
113
179
  private def draw_modal(start_row, start_col)
114
180
  # Use theme colors - cyan for border, bright_cyan for title
@@ -167,8 +233,9 @@ module Clacky
167
233
  # Show placeholder in dim gray
168
234
  display_text = @pastel.dim(placeholder)
169
235
  elsif field[:mask]
170
- # Show masked input
171
- display_text = @pastel.cyan('*' * buffer.length)
236
+ # Show masked input - limit display length to prevent overflow
237
+ mask_length = [buffer.length, input_width].min
238
+ display_text = @pastel.cyan('*' * mask_length)
172
239
  else
173
240
  # Show normal input
174
241
  display_text = @pastel.cyan(buffer)
@@ -232,13 +299,17 @@ module Clacky
232
299
 
233
300
  # Draw error message
234
301
  private def draw_error_message(row, col)
302
+ return if @error_message.nil? || @error_message.empty?
303
+
235
304
  max_width = @width - 6
236
305
  # Truncate error message if too long
237
306
  error_text = @error_message.length > max_width ? @error_message[0..max_width-4] + "..." : @error_message
238
307
  error_col = col + 3
239
308
 
240
- formatted = @pastel.red("⚠ #{error_text}")
241
- print "\e[#{row};#{error_col}H#{formatted}"
309
+ # Clear the line first to prevent leftover characters
310
+ print "\e[#{row};#{error_col}H\e[K"
311
+ formatted = @pastel.red("⚠ #{error_text}")
312
+ print formatted
242
313
  end
243
314
 
244
315
  # Draw confirmation buttons
@@ -257,6 +328,72 @@ module Clacky
257
328
  print "\e[#{start_row + i};#{start_col}H#{' ' * @width}"
258
329
  end
259
330
  end
331
+
332
+ # Draw menu choices
333
+ private def draw_menu_choices(start_row, start_col)
334
+ @choices.each_with_index do |choice, index|
335
+ row = start_row + index
336
+ draw_menu_choice(choice, index, row, start_col)
337
+ end
338
+ end
339
+
340
+ # Draw a single menu choice
341
+ private def draw_menu_choice(choice, index, row, col)
342
+ is_selected = (index == @selected_index)
343
+
344
+ # Calculate available width for text
345
+ text_width = @width - 6 # Account for borders, marker, and padding
346
+
347
+ # Prepare choice text
348
+ choice_text = choice[:name] || ""
349
+ if choice_text.length > text_width
350
+ choice_text = choice_text[0..text_width-4] + "..."
351
+ end
352
+
353
+ # Prepare marker and styling
354
+ if choice[:disabled]
355
+ # Disabled choice (like separator)
356
+ marker = " "
357
+ text = @pastel.dim(choice_text)
358
+ elsif is_selected
359
+ marker = @pastel.bright_cyan("→ ")
360
+ text = @pastel.bright_white(choice_text)
361
+ else
362
+ marker = " "
363
+ text = @pastel.white(choice_text)
364
+ end
365
+
366
+ # Draw line
367
+ print "\e[#{row};#{col}H"
368
+ print @pastel.cyan("│") + marker + text
369
+
370
+ # Pad to width
371
+ used_length = choice_text.length + 2 # marker is 2 chars
372
+ padding_needed = @width - used_length - 2 # -2 for borders
373
+ print " " * padding_needed + @pastel.cyan("│")
374
+ end
375
+
376
+ # Draw menu instructions
377
+ private def draw_menu_instructions(row, col)
378
+ instructions = "↑↓/jk: Navigate • Enter: Select • Esc/q: Cancel"
379
+ padding = (@width - instructions.length - 2) / 2
380
+ remaining = @width - padding - instructions.length - 2
381
+
382
+ print "\e[#{row};#{col}H"
383
+ border_line = @pastel.cyan("└" + "─" * padding)
384
+ instr_part = @pastel.dim(instructions)
385
+ border_rest = @pastel.cyan("─" * remaining + "┘")
386
+ print border_line + instr_part + border_rest
387
+ end
388
+
389
+ # Move menu selection up or down
390
+ private def move_menu_selection(direction)
391
+ loop do
392
+ @selected_index = (@selected_index + direction) % @choices.length
393
+ # Skip disabled choices
394
+ break unless @choices[@selected_index][:disabled]
395
+ end
396
+ end
260
397
  end
261
398
  end
262
399
  end
@@ -15,6 +15,7 @@ module Clacky
15
15
  @render_mutex = Mutex.new
16
16
  @output_row = 0 # Track current output row position
17
17
  @last_fixed_area_height = 0 # Track previous fixed area height to detect shrinkage
18
+ @fullscreen_mode = false # Track if in fullscreen mode
18
19
 
19
20
  calculate_layout
20
21
  setup_resize_handler
@@ -92,10 +93,9 @@ module Clacky
92
93
  def position_inline_input_cursor(inline_input)
93
94
  return unless inline_input
94
95
 
95
- # Use the shared method from LineEditor to calculate cursor position with wrap
96
- prompt = inline_input.prompt
96
+ # Use InlineInput's method to calculate cursor position (handles continuation prompt correctly)
97
97
  width = screen.width
98
- wrap_row, wrap_col = inline_input.cursor_position_with_wrap(prompt, width)
98
+ wrap_row, wrap_col = inline_input.cursor_position_for_display(width)
99
99
 
100
100
  # Get the number of lines InlineInput occupies (considering wrapping)
101
101
  line_count = inline_input.line_count(width)
@@ -156,7 +156,7 @@ module Clacky
156
156
  screen.move_cursor(row, 0)
157
157
  screen.clear_line
158
158
  end
159
-
159
+
160
160
  # Move cursor to start of a new line after last output
161
161
  # Use \r to ensure we're at column 0, then move down
162
162
  screen.move_cursor([@output_row, 0].max, 0)
@@ -175,10 +175,10 @@ module Clacky
175
175
  screen.move_cursor(row, 0)
176
176
  screen.clear_line
177
177
  end
178
-
178
+
179
179
  # Reset output position to beginning
180
180
  @output_row = 0
181
-
181
+
182
182
  # Re-render fixed areas to ensure they stay in place
183
183
  render_fixed_areas
184
184
  screen.flush
@@ -316,33 +316,31 @@ module Clacky
316
316
  screen.flush
317
317
  end
318
318
 
319
- private
320
-
321
319
  # Write a single line to output area
322
320
  # Handles scrolling when reaching fixed area
323
321
  # @param line [String] Single line to write (should not contain newlines)
324
322
  def write_output_line(line)
325
323
  # Calculate where fixed area starts (this is where output area ends)
326
324
  max_output_row = fixed_area_start_row
327
-
325
+
328
326
  # If we're about to write into the fixed area, scroll first
329
327
  if @output_row >= max_output_row
330
328
  # Trigger terminal scroll by printing newline at bottom
331
329
  screen.move_cursor(screen.height - 1, 0)
332
330
  print "\n"
333
-
331
+
334
332
  # After scroll, position to write at the last row of output area
335
333
  @output_row = max_output_row - 1
336
-
334
+
337
335
  # Important: Re-render fixed areas after scroll to prevent corruption
338
336
  render_fixed_areas
339
337
  end
340
-
338
+
341
339
  # Now write the line at current position
342
340
  screen.move_cursor(@output_row, 0)
343
341
  screen.clear_line
344
342
  print line
345
-
343
+
346
344
  # Move to next row for next write
347
345
  @output_row += 1
348
346
  end
@@ -353,26 +351,26 @@ module Clacky
353
351
  # @return [Array<String>] Array of wrapped lines
354
352
  def wrap_long_line(line)
355
353
  return [""] if line.nil? || line.empty?
356
-
354
+
357
355
  max_width = screen.width
358
356
  return [line] if max_width <= 0
359
-
357
+
360
358
  # Strip ANSI codes for width calculation
361
359
  visible_line = line.gsub(/\e\[[0-9;]*m/, '')
362
-
360
+
363
361
  # Check if line needs wrapping
364
362
  display_width = calculate_display_width(visible_line)
365
363
  return [line] if display_width <= max_width
366
-
364
+
367
365
  # Line needs wrapping - split by considering display width
368
366
  wrapped = []
369
367
  current_line = ""
370
368
  current_width = 0
371
369
  ansi_codes = [] # Track ANSI codes to carry over
372
-
370
+
373
371
  # Extract ANSI codes and text segments
374
372
  segments = line.split(/(\e\[[0-9;]*m)/)
375
-
373
+
376
374
  segments.each do |segment|
377
375
  if segment =~ /^\e\[[0-9;]*m$/
378
376
  # ANSI code - add to current codes
@@ -382,7 +380,7 @@ module Clacky
382
380
  # Text segment - process character by character
383
381
  segment.each_char do |char|
384
382
  char_width = char_display_width(char)
385
-
383
+
386
384
  if current_width + char_width > max_width && !current_line.empty?
387
385
  # Complete current line
388
386
  wrapped << current_line
@@ -390,19 +388,19 @@ module Clacky
390
388
  current_line = ansi_codes.join
391
389
  current_width = 0
392
390
  end
393
-
391
+
394
392
  current_line += char
395
393
  current_width += char_width
396
394
  end
397
395
  end
398
396
  end
399
-
397
+
400
398
  # Add remaining content
401
399
  wrapped << current_line unless current_line.empty? || current_line == ansi_codes.join
402
-
400
+
403
401
  wrapped.empty? ? [""] : wrapped
404
402
  end
405
-
403
+
406
404
  # Calculate display width of a single character
407
405
  # @param char [String] Single character
408
406
  # @return [Integer] Display width (1 or 2)
@@ -427,7 +425,7 @@ module Clacky
427
425
  1
428
426
  end
429
427
  end
430
-
428
+
431
429
  # Calculate display width of a string (considering multi-byte characters)
432
430
  # @param text [String] Text to calculate
433
431
  # @return [Integer] Display width
@@ -504,6 +502,58 @@ module Clacky
504
502
  screen.show_cursor
505
503
  end
506
504
 
505
+ # Restore screen from fullscreen mode (re-render everything)
506
+ def restore_screen
507
+ @render_mutex.synchronize do
508
+ screen.clear_screen
509
+ screen.hide_cursor
510
+ render_all_internal
511
+ end
512
+ end
513
+
514
+ # Check if in fullscreen mode
515
+ # @return [Boolean]
516
+ def fullscreen_mode?
517
+ @fullscreen_mode
518
+ end
519
+
520
+ # Enter fullscreen mode with alternate screen buffer
521
+ # @param lines [Array<String>] Lines to display
522
+ # @param hint [String] Hint message at bottom
523
+ def enter_fullscreen(lines, hint: "Press Ctrl+O to return")
524
+ return if @fullscreen_mode
525
+
526
+ @fullscreen_mode = true
527
+
528
+ # Enter alternate screen buffer
529
+ print "\e[?1049h"
530
+ # Clear screen and move cursor to top
531
+ print "\e[2J\e[H"
532
+ $stdout.flush
533
+
534
+ # Show all lines with proper line endings (CR+LF)
535
+ lines.each do |line|
536
+ # Strip trailing newline and print with CR+LF
537
+ print line.chomp + "\r\n"
538
+ end
539
+
540
+ # Show hint at bottom
541
+ print "\r\n"
542
+ print "\e[36m#{hint}\e[0m\r\n"
543
+ $stdout.flush
544
+ end
545
+
546
+ # Exit fullscreen mode and restore previous screen
547
+ def exit_fullscreen
548
+ return unless @fullscreen_mode
549
+
550
+ @fullscreen_mode = false
551
+
552
+ # Exit alternate screen buffer (automatically restores previous screen)
553
+ print "\e[?1049l"
554
+ $stdout.flush
555
+ end
556
+
507
557
  # Setup handler for window resize
508
558
  def setup_resize_handler
509
559
  Signal.trap("WINCH") do
@@ -7,6 +7,11 @@ module Clacky
7
7
  # LineEditor module provides single-line text editing functionality
8
8
  # Shared by InputArea and InlineInput components
9
9
  module LineEditor
10
+ # Maximum content width ratio (percentage of terminal width)
11
+ # Use 90% of terminal width for better readability on wide screens
12
+ # This dynamically adjusts based on terminal size
13
+ MAX_CONTENT_WIDTH_RATIO = 0.9
14
+
10
15
  attr_reader :cursor_position
11
16
 
12
17
  def initialize_line_editor
@@ -200,8 +205,9 @@ module Clacky
200
205
  # Get cursor position considering line wrapping
201
206
  # @param prompt [String] Prompt string before the line (may contain ANSI codes)
202
207
  # @param width [Integer] Terminal width for wrapping
208
+ # @param continuation_prompt [String] Prompt for continuation lines (default: "> ")
203
209
  # @return [Array<Integer>] Row and column position (0-indexed)
204
- def cursor_position_with_wrap(prompt = "", width = TTY::Screen.width)
210
+ def cursor_position_with_wrap(prompt = "", width = TTY::Screen.width, continuation_prompt = "> ")
205
211
  return [0, cursor_column(prompt)] if width <= 0
206
212
 
207
213
  prompt_width = calculate_display_width(strip_ansi_codes(prompt))
@@ -232,7 +238,15 @@ module Clacky
232
238
  text_in_segment_before_cursor = chars[segment_start...(segment_start + cursor_pos_in_segment)].join
233
239
  display_width = calculate_display_width(text_in_segment_before_cursor)
234
240
 
235
- col = prompt_width + display_width
241
+ # Use appropriate prompt width based on which segment (row) we're on
242
+ # First line uses original prompt, subsequent lines use continuation prompt
243
+ actual_prompt_width = if cursor_segment_idx == 0
244
+ prompt_width
245
+ else
246
+ calculate_display_width(strip_ansi_codes(continuation_prompt))
247
+ end
248
+
249
+ col = actual_prompt_width + display_width
236
250
  row = cursor_segment_idx
237
251
 
238
252
  [row, col]
@@ -309,6 +323,13 @@ module Clacky
309
323
  end
310
324
  end
311
325
 
326
+ # Calculate effective content width (respecting MAX_CONTENT_WIDTH_RATIO)
327
+ # @param screen_width [Integer] Terminal screen width
328
+ # @return [Integer] Effective content width to use
329
+ private def effective_content_width(screen_width)
330
+ (screen_width * MAX_CONTENT_WIDTH_RATIO).to_i
331
+ end
332
+
312
333
  # Render a segment of a line with cursor if cursor is in this segment
313
334
  # @param line [String] Full line text
314
335
  # @param segment_start [Integer] Start position of segment in line (char index)
@@ -17,10 +17,10 @@ module Clacky
17
17
  # Get current theme colors
18
18
  theme = ThemeManager.current_theme
19
19
 
20
- # Configure tty-markdown colors based on current theme
21
- # tty-markdown uses Pastel internally, we can configure symbols
20
+ # Configure tty-markdown with custom theme and symbols
22
21
  parsed = TTY::Markdown.parse(content,
23
- colors: theme_colors,
22
+ theme: theme_colors,
23
+ symbols: custom_symbols,
24
24
  width: TTY::Screen.width - 4 # Leave some margin
25
25
  )
26
26
 
@@ -57,21 +57,42 @@ module Clacky
57
57
  theme = ThemeManager.current_theme
58
58
 
59
59
  # Map our theme colors to tty-markdown's expected format
60
+ # Note: theme.colors values are already arrays, so we need to flatten when adding styles
60
61
  {
61
62
  # Headers use info color (cyan/blue)
62
- header: theme.colors[:info],
63
+ h1: Array(theme.colors[:info]) + [:bold],
64
+ h2: Array(theme.colors[:info]) + [:bold],
65
+ h3: Array(theme.colors[:info]),
66
+ h4: Array(theme.colors[:info]),
67
+ h5: Array(theme.colors[:info]),
68
+ h6: Array(theme.colors[:info]),
69
+ # Horizontal rule - make it subtle (dim gray)
70
+ hr: [:bright_black],
63
71
  # Code blocks use dim color
64
- code: theme.colors[:thinking],
72
+ code: Array(theme.colors[:thinking]),
65
73
  # Links use success color (green)
66
- link: theme.colors[:success],
74
+ link: Array(theme.colors[:success]),
67
75
  # Lists use default text color
68
- list: :bright_white,
76
+ list: [:bright_white],
69
77
  # Strong/bold use bright white
70
- strong: :bright_white,
78
+ strong: [:bright_white, :bold],
71
79
  # Emphasis/italic use white
72
- em: :white,
80
+ em: [:white],
73
81
  # Note/blockquote use dim color
74
- note: theme.colors[:thinking],
82
+ note: Array(theme.colors[:thinking]),
83
+ quote: Array(theme.colors[:thinking]),
84
+ }
85
+ end
86
+
87
+ # Get custom symbols for markdown rendering
88
+ # @return [Hash] Symbol configuration for tty-markdown
89
+ def custom_symbols
90
+ {
91
+ override: {
92
+ # Make horizontal rule simpler - just a line without decorative diamonds
93
+ diamond: "",
94
+ line: "-"
95
+ }
75
96
  }
76
97
  end
77
98
  end
@@ -224,10 +224,12 @@ module Clacky
224
224
  when "\u0006" then :ctrl_f
225
225
  when "\u000B" then :ctrl_k
226
226
  when "\u000C" then :ctrl_l
227
+ when "\u000F" then :ctrl_o
227
228
  when "\u0012" then :ctrl_r
228
229
  when "\u0015" then :ctrl_u
229
230
  when "\u0016" then :ctrl_v
230
231
  when "\u0017" then :ctrl_w
232
+ when "\t" then :tab
231
233
  else char
232
234
  end
233
235
  end