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
@@ -1,209 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pastel"
4
-
5
- module Clacky
6
- module UI
7
- # Matrix/hacker-style output formatter
8
- class Formatter
9
- # Hacker-style symbols (no emoji)
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
- }.freeze
26
-
27
- def initialize
28
- @pastel = Pastel.new
29
- end
30
-
31
- # Format user message
32
- def user_message(content)
33
- symbol = @pastel.bright_blue(SYMBOLS[:user])
34
- text = @pastel.blue(content)
35
- print "\n#{symbol} #{text}"
36
- end
37
-
38
- # Format assistant message
39
- def assistant_message(content)
40
- return if content.nil? || content.empty?
41
-
42
- symbol = @pastel.bright_green(SYMBOLS[:assistant])
43
- text = @pastel.white(content)
44
- puts "\n#{symbol} #{text}"
45
- end
46
-
47
- # Format tool call
48
- def tool_call(formatted_call)
49
- symbol = @pastel.bright_cyan(SYMBOLS[:tool_call])
50
- text = @pastel.cyan(formatted_call)
51
- puts "\n#{symbol} #{text}"
52
- end
53
-
54
- # Format tool result
55
- def tool_result(summary)
56
- symbol = @pastel.cyan(SYMBOLS[:tool_result])
57
- text = @pastel.white(summary)
58
- puts "#{symbol} #{text}"
59
- end
60
-
61
- # Format tool denied
62
- def tool_denied(tool_name)
63
- symbol = @pastel.bright_yellow(SYMBOLS[:tool_denied])
64
- text = @pastel.yellow("Tool denied: #{tool_name}")
65
- puts "\n#{symbol} #{text}"
66
- end
67
-
68
- # Format tool planned
69
- def tool_planned(tool_name)
70
- symbol = @pastel.bright_blue(SYMBOLS[:tool_planned])
71
- text = @pastel.blue("Planned: #{tool_name}")
72
- puts "\n#{symbol} #{text}"
73
- end
74
-
75
- # Format tool error
76
- def tool_error(error_message)
77
- symbol = @pastel.bright_red(SYMBOLS[:tool_error])
78
- text = @pastel.red("Error: #{error_message}")
79
- puts "\n#{symbol} #{text}"
80
- end
81
-
82
- # Format thinking indicator
83
- def thinking
84
- symbol = @pastel.dim(SYMBOLS[:thinking])
85
- # Output symbol on the same line, progress indicator will follow
86
- print "\n#{symbol} "
87
- end
88
-
89
- # Format success message
90
- def success(message)
91
- symbol = @pastel.bright_green(SYMBOLS[:success])
92
- text = @pastel.green(message)
93
- puts "#{symbol} #{text}"
94
- end
95
-
96
- # Format error message
97
- def error(message)
98
- symbol = @pastel.bright_red(SYMBOLS[:error])
99
- text = @pastel.red(message)
100
- puts "#{symbol} #{text}"
101
- end
102
-
103
- # Format warning message
104
- def warning(message)
105
- symbol = @pastel.bright_yellow(SYMBOLS[:warning])
106
- text = @pastel.yellow(message)
107
- puts "#{symbol} #{text}"
108
- end
109
-
110
- # Format info message
111
- def info(message)
112
- symbol = @pastel.bright_white(SYMBOLS[:info])
113
- text = @pastel.white(message)
114
- puts "#{symbol} #{text}"
115
- end
116
-
117
- # Format TODO status with progress bar
118
- def todo_status(todos)
119
- return if todos.empty?
120
-
121
- completed = todos.count { |t| t[:status] == "completed" }
122
- total = todos.size
123
-
124
- # Build progress bar with hacker style
125
- progress_bar = todos.map { |t|
126
- t[:status] == "completed" ? @pastel.green("█") : @pastel.dim("░")
127
- }.join
128
-
129
- # Check if all completed
130
- if completed == total
131
- symbol = @pastel.bright_green(SYMBOLS[:success])
132
- puts "\n#{symbol} Tasks [#{completed}/#{total}]: #{progress_bar} #{@pastel.bright_green('COMPLETE')}"
133
- return
134
- end
135
-
136
- # Find current and next tasks
137
- current_task = todos.find { |t| t[:status] == "pending" }
138
- next_task_index = todos.index(current_task)
139
- next_task = next_task_index && todos[next_task_index + 1]
140
-
141
- symbol = @pastel.bright_yellow(SYMBOLS[:task])
142
- puts "\n#{symbol} Tasks [#{completed}/#{total}]: #{progress_bar}"
143
-
144
- if current_task
145
- puts " #{@pastel.cyan('→')} Next: ##{current_task[:id]} - #{current_task[:task]}"
146
- end
147
-
148
- if next_task && next_task[:status] == "pending"
149
- puts " #{@pastel.dim('⇢')} After: ##{next_task[:id]} - #{next_task[:task]}"
150
- end
151
- end
152
-
153
- # Format iteration indicator
154
- def iteration(number)
155
- symbol = @pastel.dim(SYMBOLS[:progress])
156
- text = @pastel.dim("Iteration #{number}")
157
- puts "\n#{symbol} #{text}"
158
- end
159
-
160
- # Format separator
161
- def separator(char = "─", width: 80)
162
- puts @pastel.dim(char * width)
163
- end
164
-
165
- # Format section header
166
- def section_header(title)
167
- puts
168
- separator("═")
169
- puts @pastel.bright_white(title.center(80))
170
- separator("═")
171
- puts
172
- end
173
-
174
- # Format confirmation prompt for tool use
175
- def tool_confirmation_prompt(formatted_call)
176
- symbol = @pastel.bright_yellow("[??]")
177
- puts "\n#{symbol} #{@pastel.yellow(formatted_call)}"
178
- end
179
-
180
- # Format conversation history message
181
- def history_message(role, content, index, total)
182
- case role
183
- when "user"
184
- symbol = @pastel.blue(SYMBOLS[:user])
185
- text = @pastel.white(truncate(content, 150))
186
- puts "#{symbol} You: #{text}"
187
- when "assistant"
188
- symbol = @pastel.green(SYMBOLS[:assistant])
189
- text = @pastel.white(truncate(content, 200))
190
- puts "#{symbol} Assistant: #{text}"
191
- end
192
- end
193
-
194
- private
195
-
196
- def truncate(content, max_length)
197
- return "" if content.nil? || content.empty?
198
-
199
- cleaned = content.strip.gsub(/\s+/, ' ')
200
-
201
- if cleaned.length > max_length
202
- cleaned[0...max_length] + "..."
203
- else
204
- cleaned
205
- end
206
- end
207
- end
208
- end
209
- end
@@ -1,96 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "pastel"
4
- require "tty-screen"
5
-
6
- module Clacky
7
- module UI
8
- # Status bar showing session information
9
- class StatusBar
10
- def initialize
11
- @pastel = Pastel.new
12
- end
13
-
14
- # Display session status bar
15
- # @param working_dir [String] Current working directory
16
- # @param mode [String] Permission mode
17
- # @param model [String] AI model name
18
- # @param tasks [Integer] Number of completed tasks (optional)
19
- # @param cost [Float] Total cost (optional)
20
- def display(working_dir:, mode:, model:, tasks: nil, cost: nil)
21
- parts = []
22
-
23
- # Working directory (shortened if too long)
24
- dir_display = shorten_path(working_dir)
25
- parts << @pastel.bright_cyan(dir_display)
26
-
27
- # Permission mode
28
- mode_color = mode_color_for(mode)
29
- parts << @pastel.public_send(mode_color, mode)
30
-
31
- # Model
32
- parts << @pastel.bright_white(model)
33
-
34
- # Optional: tasks and cost
35
- if tasks
36
- parts << @pastel.yellow("#{tasks} tasks")
37
- end
38
-
39
- if cost
40
- parts << @pastel.yellow("$#{cost.round(4)}")
41
- end
42
-
43
- # Join with separator
44
- separator = @pastel.dim(" │ ")
45
- status_line = " " + parts.join(separator)
46
-
47
- puts status_line
48
- end
49
-
50
- # Display minimal status for non-interactive mode
51
- def display_minimal(working_dir:, mode:)
52
- dir_display = shorten_path(working_dir)
53
- puts " #{@pastel.bright_cyan(dir_display)} #{@pastel.dim('│')} #{@pastel.yellow(mode)}"
54
- end
55
-
56
- private
57
-
58
- def shorten_path(path)
59
- return path if path.length <= 40
60
-
61
- # Replace home directory with ~
62
- home = ENV['HOME']
63
- if home && path.start_with?(home)
64
- path = path.sub(home, '~')
65
- end
66
-
67
- # If still too long, show last parts
68
- if path.length > 40
69
- parts = path.split('/')
70
- if parts.length > 3
71
- ".../" + parts[-3..-1].join('/')
72
- else
73
- path[0..40] + "..."
74
- end
75
- else
76
- path
77
- end
78
- end
79
-
80
- def mode_color_for(mode)
81
- case mode.to_s
82
- when /auto_approve/
83
- :bright_red
84
- when /confirm_safes/
85
- :bright_yellow
86
- when /confirm_edits/
87
- :bright_green
88
- when /plan_only/
89
- :bright_blue
90
- else
91
- :white
92
- end
93
- end
94
- end
95
- end
96
- end