openclacky 0.5.6 → 0.6.0

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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/docs/ui2-architecture.md +124 -0
  4. data/lib/clacky/agent.rb +245 -340
  5. data/lib/clacky/agent_config.rb +1 -7
  6. data/lib/clacky/cli.rb +156 -397
  7. data/lib/clacky/client.rb +68 -36
  8. data/lib/clacky/gitignore_parser.rb +26 -12
  9. data/lib/clacky/model_pricing.rb +6 -2
  10. data/lib/clacky/session_manager.rb +6 -2
  11. data/lib/clacky/tools/glob.rb +65 -9
  12. data/lib/clacky/tools/grep.rb +4 -120
  13. data/lib/clacky/tools/run_project.rb +5 -0
  14. data/lib/clacky/tools/safe_shell.rb +49 -13
  15. data/lib/clacky/tools/shell.rb +1 -49
  16. data/lib/clacky/tools/web_fetch.rb +2 -2
  17. data/lib/clacky/tools/web_search.rb +38 -26
  18. data/lib/clacky/ui2/README.md +214 -0
  19. data/lib/clacky/ui2/components/base_component.rb +163 -0
  20. data/lib/clacky/ui2/components/common_component.rb +89 -0
  21. data/lib/clacky/ui2/components/inline_input.rb +187 -0
  22. data/lib/clacky/ui2/components/input_area.rb +1029 -0
  23. data/lib/clacky/ui2/components/message_component.rb +76 -0
  24. data/lib/clacky/ui2/components/output_area.rb +112 -0
  25. data/lib/clacky/ui2/components/todo_area.rb +137 -0
  26. data/lib/clacky/ui2/components/tool_component.rb +106 -0
  27. data/lib/clacky/ui2/components/welcome_banner.rb +93 -0
  28. data/lib/clacky/ui2/layout_manager.rb +331 -0
  29. data/lib/clacky/ui2/line_editor.rb +201 -0
  30. data/lib/clacky/ui2/screen_buffer.rb +238 -0
  31. data/lib/clacky/ui2/theme_manager.rb +68 -0
  32. data/lib/clacky/ui2/themes/base_theme.rb +99 -0
  33. data/lib/clacky/ui2/themes/hacker_theme.rb +56 -0
  34. data/lib/clacky/ui2/themes/minimal_theme.rb +50 -0
  35. data/lib/clacky/ui2/ui_controller.rb +720 -0
  36. data/lib/clacky/ui2/view_renderer.rb +160 -0
  37. data/lib/clacky/ui2.rb +37 -0
  38. data/lib/clacky/utils/file_ignore_helper.rb +126 -0
  39. data/lib/clacky/version.rb +1 -1
  40. data/lib/clacky.rb +1 -6
  41. metadata +38 -6
  42. data/lib/clacky/ui/banner.rb +0 -155
  43. data/lib/clacky/ui/enhanced_prompt.rb +0 -786
  44. data/lib/clacky/ui/formatter.rb +0 -209
  45. data/lib/clacky/ui/statusbar.rb +0 -96
@@ -0,0 +1,238 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "tty-screen"
4
+ require "io/console"
5
+
6
+ module Clacky
7
+ module UI2
8
+ # ScreenBuffer manages terminal screen state and provides low-level rendering primitives
9
+ class ScreenBuffer
10
+ attr_reader :width, :height
11
+
12
+ def initialize
13
+ @width = TTY::Screen.width
14
+ @height = TTY::Screen.height
15
+ @buffer = []
16
+ @last_input_time = nil
17
+ @rapid_input_threshold = 0.01 # 10ms threshold for detecting paste-like rapid input
18
+ setup_resize_handler
19
+ end
20
+
21
+ # Move cursor to specific position (0-indexed)
22
+ # @param row [Integer] Row position
23
+ # @param col [Integer] Column position
24
+ def move_cursor(row, col)
25
+ print "\e[#{row + 1};#{col + 1}H"
26
+ end
27
+
28
+ # Clear entire screen
29
+ def clear_screen
30
+ print "\e[2J"
31
+ move_cursor(0, 0)
32
+ end
33
+
34
+ # Clear current line
35
+ def clear_line
36
+ print "\e[2K"
37
+ end
38
+
39
+ # Clear from cursor to end of line
40
+ def clear_to_eol
41
+ print "\e[K"
42
+ end
43
+
44
+ # Hide cursor
45
+ def hide_cursor
46
+ print "\e[?25l"
47
+ end
48
+
49
+ # Show cursor
50
+ def show_cursor
51
+ print "\e[?25h"
52
+ end
53
+
54
+ # Save cursor position
55
+ def save_cursor
56
+ print "\e[s"
57
+ end
58
+
59
+ # Restore cursor position
60
+ def restore_cursor
61
+ print "\e[u"
62
+ end
63
+
64
+ # Enable alternative screen buffer (like vim/less)
65
+ def enable_alt_screen
66
+ print "\e[?1049h"
67
+ end
68
+
69
+ # Disable alternative screen buffer
70
+ def disable_alt_screen
71
+ print "\e[?1049l"
72
+ end
73
+
74
+ # Set scroll region (DECSTBM - DEC Set Top and Bottom Margins)
75
+ # Content written in this region will scroll, content outside will stay fixed
76
+ # @param top [Integer] Top row (1-indexed)
77
+ # @param bottom [Integer] Bottom row (1-indexed)
78
+ def set_scroll_region(top, bottom)
79
+ print "\e[#{top};#{bottom}r"
80
+ end
81
+
82
+ # Reset scroll region to full screen
83
+ def reset_scroll_region
84
+ print "\e[r"
85
+ end
86
+
87
+ # Scroll the scroll region up by n lines
88
+ # @param n [Integer] Number of lines to scroll
89
+ def scroll_up(n = 1)
90
+ print "\e[#{n}S"
91
+ end
92
+
93
+ # Scroll the scroll region down by n lines
94
+ # @param n [Integer] Number of lines to scroll
95
+ def scroll_down(n = 1)
96
+ print "\e[#{n}T"
97
+ end
98
+
99
+ # Get current screen dimensions
100
+ def update_dimensions
101
+ @width = TTY::Screen.width
102
+ @height = TTY::Screen.height
103
+ end
104
+
105
+ # Enable raw mode (disable line buffering)
106
+ def enable_raw_mode
107
+ $stdin.raw!
108
+ end
109
+
110
+ # Disable raw mode
111
+ def disable_raw_mode
112
+ $stdin.cooked!
113
+ end
114
+
115
+ # Read a single character without echo
116
+ # @param timeout [Float] Timeout in seconds (nil for blocking)
117
+ # @return [String, nil] Character or nil if timeout
118
+ def read_char(timeout: nil)
119
+ if timeout
120
+ return nil unless IO.select([$stdin], nil, nil, timeout)
121
+ end
122
+
123
+ $stdin.getc
124
+ end
125
+
126
+ # Read a key including special keys (arrows, etc.)
127
+ # @param timeout [Float] Timeout in seconds
128
+ # @return [Symbol, String, Hash, nil] Key symbol, character, or { type: :rapid_input, text: String }
129
+ def read_key(timeout: nil)
130
+ $stdin.set_encoding('UTF-8')
131
+
132
+ current_time = Time.now.to_f
133
+ is_rapid_input = @last_input_time && (current_time - @last_input_time) < @rapid_input_threshold
134
+ @last_input_time = current_time
135
+
136
+ char = read_char(timeout: timeout)
137
+ return nil unless char
138
+
139
+ # Ensure character is UTF-8 encoded
140
+ char = char.force_encoding('UTF-8') if char.is_a?(String) && char.encoding != Encoding::UTF_8
141
+
142
+ # Handle escape sequences for special keys
143
+ if char == "\e"
144
+ # Non-blocking read for escape sequence
145
+ char2 = read_char(timeout: 0.01)
146
+ return :escape unless char2
147
+
148
+ if char2 == "["
149
+ char3 = read_char(timeout: 0.01)
150
+ case char3
151
+ when "A" then return :up_arrow
152
+ when "B" then return :down_arrow
153
+ when "C" then return :right_arrow
154
+ when "D" then return :left_arrow
155
+ when "H" then return :home
156
+ when "F" then return :end
157
+ when "Z" then return :shift_tab
158
+ when "3"
159
+ char4 = read_char(timeout: 0.01)
160
+ return :delete if char4 == "~"
161
+ end
162
+ end
163
+ end
164
+
165
+ # Check if there are more characters available (for rapid input detection)
166
+ has_more_input = IO.select([$stdin], nil, nil, 0)
167
+
168
+ # If this is rapid input or there are more characters available
169
+ if is_rapid_input || has_more_input
170
+ buffer = char.to_s.dup
171
+ buffer.force_encoding('UTF-8')
172
+
173
+ # Keep reading available characters
174
+ loop do
175
+ break unless IO.select([$stdin], nil, nil, 0)
176
+
177
+ next_char = $stdin.getc
178
+ break unless next_char
179
+
180
+ next_char = next_char.force_encoding('UTF-8') if next_char.encoding != Encoding::UTF_8
181
+ buffer << next_char
182
+ end
183
+
184
+ # If we buffered multiple characters or newlines, treat as rapid input (paste)
185
+ if buffer.length > 1 || buffer.include?("\n") || buffer.include?("\r")
186
+ # Remove any trailing \r or \n from rapid input buffer
187
+ cleaned_buffer = buffer.gsub(/[\r\n]+\z/, '')
188
+ return { type: :rapid_input, text: cleaned_buffer } if cleaned_buffer.length > 0
189
+ end
190
+
191
+ # Single character, continue to normal handling
192
+ char = buffer[0] if buffer.length == 1
193
+ end
194
+
195
+ # Handle control characters
196
+ case char
197
+ when "\r" then :enter
198
+ when "\n" then :newline # Shift+Enter sends \n
199
+ when "\u007F", "\b" then :backspace
200
+ when "\u0001" then :ctrl_a
201
+ when "\u0002" then :ctrl_b
202
+ when "\u0003" then :ctrl_c
203
+ when "\u0004" then :ctrl_d
204
+ when "\u0005" then :ctrl_e
205
+ when "\u0006" then :ctrl_f
206
+ when "\u000B" then :ctrl_k
207
+ when "\u000C" then :ctrl_l
208
+ when "\u0012" then :ctrl_r
209
+ when "\u0015" then :ctrl_u
210
+ when "\u0016" then :ctrl_v
211
+ when "\u0017" then :ctrl_w
212
+ else char
213
+ end
214
+ end
215
+
216
+ # Flush output
217
+ def flush
218
+ $stdout.flush
219
+ end
220
+
221
+ private
222
+
223
+ # Setup handler for terminal resize (SIGWINCH)
224
+ def setup_resize_handler
225
+ Signal.trap("WINCH") do
226
+ update_dimensions
227
+ @resize_callback&.call(@width, @height)
228
+ end
229
+ end
230
+
231
+ # Register callback for resize events
232
+ # @param block [Proc] Callback to execute on resize
233
+ def on_resize(&block)
234
+ @resize_callback = block
235
+ end
236
+ end
237
+ end
238
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "themes/base_theme"
4
+ require_relative "themes/hacker_theme"
5
+ require_relative "themes/minimal_theme"
6
+
7
+ module Clacky
8
+ module UI2
9
+ # ThemeManager handles theme registration and switching
10
+ class ThemeManager
11
+ class << self
12
+ def instance
13
+ @instance ||= new
14
+ end
15
+
16
+ # Delegate methods to instance
17
+ def current_theme
18
+ instance.current_theme
19
+ end
20
+
21
+ def set_theme(name)
22
+ instance.set_theme(name)
23
+ end
24
+
25
+ def available_themes
26
+ instance.available_themes
27
+ end
28
+
29
+ def register_theme(name, theme_class)
30
+ instance.register_theme(name, theme_class)
31
+ end
32
+ end
33
+
34
+ def initialize
35
+ @themes = {}
36
+ @current_theme = nil
37
+ register_default_themes
38
+ set_theme(:hacker)
39
+ end
40
+
41
+ def current_theme
42
+ @current_theme
43
+ end
44
+
45
+ def set_theme(name)
46
+ name = name.to_sym
47
+ raise ArgumentError, "Unknown theme: #{name}" unless @themes.key?(name)
48
+
49
+ @current_theme = @themes[name].new
50
+ end
51
+
52
+ def available_themes
53
+ @themes.keys
54
+ end
55
+
56
+ def register_theme(name, theme_class)
57
+ @themes[name.to_sym] = theme_class
58
+ end
59
+
60
+ private
61
+
62
+ def register_default_themes
63
+ register_theme(:hacker, Themes::HackerTheme)
64
+ register_theme(:minimal, Themes::MinimalTheme)
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "pastel"
4
+
5
+ module Clacky
6
+ module UI2
7
+ module Themes
8
+ # BaseTheme defines the interface for all themes
9
+ # Subclasses should override SYMBOLS and color methods
10
+ class BaseTheme
11
+ SYMBOLS = {
12
+ user: "[>>]",
13
+ assistant: "[<<]",
14
+ tool_call: "[=>]",
15
+ tool_result: "[<=]",
16
+ tool_denied: "[!!]",
17
+ tool_planned: "[??]",
18
+ tool_error: "[XX]",
19
+ thinking: "[..]",
20
+ success: "[OK]",
21
+ error: "[ER]",
22
+ warning: "[!!]",
23
+ info: "[--]",
24
+ task: "[##]",
25
+ progress: "[>>]",
26
+ file: "[F]",
27
+ command: "[C]",
28
+ cached: "[*]"
29
+ }.freeze
30
+
31
+ # Color schemes for different elements
32
+ # Each returns [symbol_color, text_color]
33
+ COLORS = {
34
+ user: [:bright_blue, :blue],
35
+ assistant: [:bright_green, :white],
36
+ tool_call: [:bright_cyan, :cyan],
37
+ tool_result: [:cyan, :white],
38
+ tool_denied: [:bright_yellow, :yellow],
39
+ tool_planned: [:bright_blue, :blue],
40
+ tool_error: [:bright_red, :red],
41
+ thinking: [:dim, :dim],
42
+ success: [:bright_green, :green],
43
+ error: [:bright_red, :red],
44
+ warning: [:bright_yellow, :yellow],
45
+ info: [:bright_white, :white],
46
+ task: [:bright_yellow, :white],
47
+ progress: [:bright_cyan, :cyan],
48
+ file: [:cyan, :white],
49
+ command: [:cyan, :white],
50
+ cached: [:cyan, :cyan]
51
+ }.freeze
52
+
53
+ def initialize
54
+ @pastel = Pastel.new
55
+ end
56
+
57
+ def symbols
58
+ self.class::SYMBOLS
59
+ end
60
+
61
+ def colors
62
+ self.class::COLORS
63
+ end
64
+
65
+ def symbol(key)
66
+ symbols[key] || "[??]"
67
+ end
68
+
69
+ def symbol_color(key)
70
+ colors.dig(key, 0) || :white
71
+ end
72
+
73
+ def text_color(key)
74
+ colors.dig(key, 1) || :white
75
+ end
76
+
77
+ # Format symbol with its color
78
+ # @param key [Symbol] Symbol key (e.g., :user, :assistant)
79
+ # @return [String] Colored symbol
80
+ def format_symbol(key)
81
+ @pastel.public_send(symbol_color(key), symbol(key))
82
+ end
83
+
84
+ # Format text with color for given key
85
+ # @param text [String] Text to format
86
+ # @param key [Symbol] Color key (e.g., :user, :assistant)
87
+ # @return [String] Colored text
88
+ def format_text(text, key)
89
+ @pastel.public_send(text_color(key), text)
90
+ end
91
+
92
+ # Theme name for display
93
+ def name
94
+ "base"
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_theme"
4
+
5
+ module Clacky
6
+ module UI2
7
+ module Themes
8
+ # HackerTheme - Matrix/hacker-style with bracket symbols
9
+ class HackerTheme < BaseTheme
10
+ SYMBOLS = {
11
+ user: "[>>]",
12
+ assistant: "[<<]",
13
+ tool_call: "[=>]",
14
+ tool_result: "[<=]",
15
+ tool_denied: "[!!]",
16
+ tool_planned: "[??]",
17
+ tool_error: "[XX]",
18
+ thinking: "[..]",
19
+ success: "[OK]",
20
+ error: "[ER]",
21
+ warning: "[!!]",
22
+ info: "[--]",
23
+ task: "[##]",
24
+ progress: "[>>]",
25
+ file: "[F]",
26
+ command: "[C]",
27
+ cached: "[*]"
28
+ }.freeze
29
+
30
+ COLORS = {
31
+ user: [:bright_blue, :blue],
32
+ assistant: [:bright_green, :white],
33
+ tool_call: [:bright_cyan, :cyan],
34
+ tool_result: [:cyan, :white],
35
+ tool_denied: [:bright_yellow, :yellow],
36
+ tool_planned: [:bright_blue, :blue],
37
+ tool_error: [:bright_red, :red],
38
+ thinking: [:dim, :dim],
39
+ success: [:bright_green, :green],
40
+ error: [:bright_red, :red],
41
+ warning: [:bright_yellow, :yellow],
42
+ info: [:bright_white, :white],
43
+ task: [:bright_yellow, :white],
44
+ progress: [:bright_cyan, :cyan],
45
+ file: [:cyan, :white],
46
+ command: [:cyan, :white],
47
+ cached: [:cyan, :cyan]
48
+ }.freeze
49
+
50
+ def name
51
+ "hacker"
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "base_theme"
4
+
5
+ module Clacky
6
+ module UI2
7
+ module Themes
8
+ # MinimalTheme - Clean, simple symbols
9
+ class MinimalTheme < BaseTheme
10
+ SYMBOLS = {
11
+ user: ">",
12
+ assistant: "<",
13
+ tool_call: "*",
14
+ tool_result: "-",
15
+ tool_denied: "!",
16
+ tool_planned: "?",
17
+ tool_error: "x",
18
+ thinking: ".",
19
+ success: "+",
20
+ error: "x",
21
+ warning: "!",
22
+ info: "-",
23
+ task: "#",
24
+ progress: ">"
25
+ }.freeze
26
+
27
+ COLORS = {
28
+ user: [:blue, :blue],
29
+ assistant: [:green, :white],
30
+ tool_call: [:cyan, :cyan],
31
+ tool_result: [:white, :white],
32
+ tool_denied: [:yellow, :yellow],
33
+ tool_planned: [:blue, :blue],
34
+ tool_error: [:red, :red],
35
+ thinking: [:dim, :dim],
36
+ success: [:green, :green],
37
+ error: [:red, :red],
38
+ warning: [:yellow, :yellow],
39
+ info: [:white, :white],
40
+ task: [:yellow, :white],
41
+ progress: [:cyan, :cyan]
42
+ }.freeze
43
+
44
+ def name
45
+ "minimal"
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end