openclacky 0.6.0 → 0.6.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +54 -0
- data/README.md +39 -88
- data/homebrew/README.md +96 -0
- data/homebrew/openclacky.rb +24 -0
- data/lib/clacky/agent.rb +139 -67
- data/lib/clacky/cli.rb +105 -6
- data/lib/clacky/tools/file_reader.rb +135 -2
- data/lib/clacky/tools/glob.rb +2 -2
- data/lib/clacky/tools/grep.rb +2 -2
- data/lib/clacky/tools/run_project.rb +5 -5
- data/lib/clacky/tools/safe_shell.rb +140 -17
- data/lib/clacky/tools/shell.rb +69 -2
- data/lib/clacky/tools/todo_manager.rb +50 -3
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/web_fetch.rb +2 -2
- data/lib/clacky/tools/web_search.rb +2 -2
- data/lib/clacky/ui2/components/common_component.rb +14 -5
- data/lib/clacky/ui2/components/input_area.rb +300 -89
- data/lib/clacky/ui2/components/message_component.rb +7 -3
- data/lib/clacky/ui2/components/todo_area.rb +38 -45
- data/lib/clacky/ui2/components/welcome_banner.rb +10 -0
- data/lib/clacky/ui2/layout_manager.rb +180 -50
- data/lib/clacky/ui2/markdown_renderer.rb +80 -0
- data/lib/clacky/ui2/screen_buffer.rb +26 -7
- data/lib/clacky/ui2/themes/base_theme.rb +32 -46
- data/lib/clacky/ui2/themes/hacker_theme.rb +4 -2
- data/lib/clacky/ui2/themes/minimal_theme.rb +4 -2
- data/lib/clacky/ui2/ui_controller.rb +150 -32
- data/lib/clacky/ui2/view_renderer.rb +21 -4
- data/lib/clacky/ui2.rb +0 -1
- data/lib/clacky/utils/arguments_parser.rb +7 -2
- data/lib/clacky/utils/file_processor.rb +201 -0
- data/lib/clacky/version.rb +1 -1
- data/scripts/install.sh +249 -0
- data/scripts/uninstall.sh +146 -0
- metadata +21 -2
- data/lib/clacky/ui2/components/output_area.rb +0 -112
|
@@ -5,71 +5,43 @@ require "pastel"
|
|
|
5
5
|
module Clacky
|
|
6
6
|
module UI2
|
|
7
7
|
module Themes
|
|
8
|
-
# BaseTheme defines the interface for all themes
|
|
9
|
-
# Subclasses
|
|
8
|
+
# BaseTheme defines the abstract interface for all themes
|
|
9
|
+
# Subclasses MUST define SYMBOLS and COLORS constants
|
|
10
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
11
|
def initialize
|
|
54
12
|
@pastel = Pastel.new
|
|
13
|
+
validate_theme_definition!
|
|
55
14
|
end
|
|
56
15
|
|
|
16
|
+
# Get all symbols defined by this theme
|
|
17
|
+
# @return [Hash] Symbol definitions
|
|
57
18
|
def symbols
|
|
58
19
|
self.class::SYMBOLS
|
|
59
20
|
end
|
|
60
21
|
|
|
22
|
+
# Get all colors defined by this theme
|
|
23
|
+
# @return [Hash] Color definitions
|
|
61
24
|
def colors
|
|
62
25
|
self.class::COLORS
|
|
63
26
|
end
|
|
64
27
|
|
|
28
|
+
# Get symbol for a specific key
|
|
29
|
+
# @param key [Symbol] Symbol key
|
|
30
|
+
# @return [String] Symbol string
|
|
65
31
|
def symbol(key)
|
|
66
32
|
symbols[key] || "[??]"
|
|
67
33
|
end
|
|
68
34
|
|
|
35
|
+
# Get symbol color for a specific key
|
|
36
|
+
# @param key [Symbol] Color key
|
|
37
|
+
# @return [Symbol] Pastel color method name
|
|
69
38
|
def symbol_color(key)
|
|
70
39
|
colors.dig(key, 0) || :white
|
|
71
40
|
end
|
|
72
41
|
|
|
42
|
+
# Get text color for a specific key
|
|
43
|
+
# @param key [Symbol] Color key
|
|
44
|
+
# @return [Symbol] Pastel color method name
|
|
73
45
|
def text_color(key)
|
|
74
46
|
colors.dig(key, 1) || :white
|
|
75
47
|
end
|
|
@@ -89,9 +61,23 @@ module Clacky
|
|
|
89
61
|
@pastel.public_send(text_color(key), text)
|
|
90
62
|
end
|
|
91
63
|
|
|
92
|
-
# Theme name for display
|
|
64
|
+
# Theme name for display (subclasses should override)
|
|
65
|
+
# @return [String] Theme name
|
|
93
66
|
def name
|
|
94
|
-
"
|
|
67
|
+
raise NotImplementedError, "Subclass must implement #name method"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
# Validate that subclass has defined required constants
|
|
73
|
+
def validate_theme_definition!
|
|
74
|
+
unless self.class.const_defined?(:SYMBOLS)
|
|
75
|
+
raise NotImplementedError, "Theme #{self.class.name} must define SYMBOLS constant"
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
unless self.class.const_defined?(:COLORS)
|
|
79
|
+
raise NotImplementedError, "Theme #{self.class.name} must define COLORS constant"
|
|
80
|
+
end
|
|
95
81
|
end
|
|
96
82
|
end
|
|
97
83
|
end
|
|
@@ -16,6 +16,7 @@ module Clacky
|
|
|
16
16
|
tool_planned: "[??]",
|
|
17
17
|
tool_error: "[XX]",
|
|
18
18
|
thinking: "[..]",
|
|
19
|
+
working: "[..]",
|
|
19
20
|
success: "[OK]",
|
|
20
21
|
error: "[ER]",
|
|
21
22
|
warning: "[!!]",
|
|
@@ -28,14 +29,15 @@ module Clacky
|
|
|
28
29
|
}.freeze
|
|
29
30
|
|
|
30
31
|
COLORS = {
|
|
31
|
-
user: [:
|
|
32
|
+
user: [:white, :white],
|
|
32
33
|
assistant: [:bright_green, :white],
|
|
33
34
|
tool_call: [:bright_cyan, :cyan],
|
|
34
35
|
tool_result: [:cyan, :white],
|
|
35
36
|
tool_denied: [:bright_yellow, :yellow],
|
|
36
|
-
tool_planned: [:
|
|
37
|
+
tool_planned: [:bright_cyan, :cyan],
|
|
37
38
|
tool_error: [:bright_red, :red],
|
|
38
39
|
thinking: [:dim, :dim],
|
|
40
|
+
working: [:bright_yellow, :yellow],
|
|
39
41
|
success: [:bright_green, :green],
|
|
40
42
|
error: [:bright_red, :red],
|
|
41
43
|
warning: [:bright_yellow, :yellow],
|
|
@@ -16,6 +16,7 @@ module Clacky
|
|
|
16
16
|
tool_planned: "?",
|
|
17
17
|
tool_error: "x",
|
|
18
18
|
thinking: ".",
|
|
19
|
+
working: ".",
|
|
19
20
|
success: "+",
|
|
20
21
|
error: "x",
|
|
21
22
|
warning: "!",
|
|
@@ -25,14 +26,15 @@ module Clacky
|
|
|
25
26
|
}.freeze
|
|
26
27
|
|
|
27
28
|
COLORS = {
|
|
28
|
-
user: [:
|
|
29
|
+
user: [:white, :white],
|
|
29
30
|
assistant: [:green, :white],
|
|
30
31
|
tool_call: [:cyan, :cyan],
|
|
31
32
|
tool_result: [:white, :white],
|
|
32
33
|
tool_denied: [:yellow, :yellow],
|
|
33
|
-
tool_planned: [:
|
|
34
|
+
tool_planned: [:cyan, :cyan],
|
|
34
35
|
tool_error: [:red, :red],
|
|
35
36
|
thinking: [:dim, :dim],
|
|
37
|
+
working: [:bright_yellow, :yellow],
|
|
36
38
|
success: [:green, :green],
|
|
37
39
|
error: [:red, :red],
|
|
38
40
|
warning: [:yellow, :yellow],
|
|
@@ -2,7 +2,6 @@
|
|
|
2
2
|
|
|
3
3
|
require_relative "layout_manager"
|
|
4
4
|
require_relative "view_renderer"
|
|
5
|
-
require_relative "components/output_area"
|
|
6
5
|
require_relative "components/input_area"
|
|
7
6
|
require_relative "components/todo_area"
|
|
8
7
|
require_relative "components/welcome_banner"
|
|
@@ -31,13 +30,11 @@ module Clacky
|
|
|
31
30
|
}
|
|
32
31
|
|
|
33
32
|
# Initialize layout components
|
|
34
|
-
@output_area = Components::OutputArea.new(height: 20) # Will be recalculated
|
|
35
33
|
@input_area = Components::InputArea.new
|
|
36
34
|
@todo_area = Components::TodoArea.new
|
|
37
35
|
@welcome_banner = Components::WelcomeBanner.new
|
|
38
36
|
@inline_input = nil # Created when needed
|
|
39
37
|
@layout = LayoutManager.new(
|
|
40
|
-
output_area: @output_area,
|
|
41
38
|
input_area: @input_area,
|
|
42
39
|
todo_area: @todo_area
|
|
43
40
|
)
|
|
@@ -59,7 +56,8 @@ module Clacky
|
|
|
59
56
|
end
|
|
60
57
|
|
|
61
58
|
# Initialize screen and show banner (separate from input loop)
|
|
62
|
-
|
|
59
|
+
# @param recent_user_messages [Array<String>, nil] Recent user messages when loading session
|
|
60
|
+
def initialize_and_show_banner(recent_user_messages: nil)
|
|
63
61
|
@running = true
|
|
64
62
|
|
|
65
63
|
# Set session bar data before initializing screen
|
|
@@ -73,8 +71,12 @@ module Clacky
|
|
|
73
71
|
|
|
74
72
|
@layout.initialize_screen
|
|
75
73
|
|
|
76
|
-
# Display welcome banner
|
|
77
|
-
|
|
74
|
+
# Display welcome banner or session history
|
|
75
|
+
if recent_user_messages && !recent_user_messages.empty?
|
|
76
|
+
display_session_history(recent_user_messages)
|
|
77
|
+
else
|
|
78
|
+
display_welcome_banner
|
|
79
|
+
end
|
|
78
80
|
end
|
|
79
81
|
|
|
80
82
|
# Start input loop (separate from initialization)
|
|
@@ -206,19 +208,21 @@ module Clacky
|
|
|
206
208
|
# - cost: cost for this iteration
|
|
207
209
|
def show_token_usage(token_data)
|
|
208
210
|
theme = ThemeManager.current_theme
|
|
211
|
+
pastel = Pastel.new
|
|
209
212
|
|
|
210
213
|
token_info = []
|
|
211
214
|
|
|
212
|
-
# Delta tokens with color coding
|
|
215
|
+
# Delta tokens with color coding (green/yellow/red + dim)
|
|
213
216
|
delta_tokens = token_data[:delta_tokens]
|
|
214
217
|
delta_str = "+#{delta_tokens}"
|
|
215
|
-
|
|
216
|
-
|
|
218
|
+
color_style = if delta_tokens > 10000
|
|
219
|
+
:red
|
|
217
220
|
elsif delta_tokens > 5000
|
|
218
|
-
|
|
221
|
+
:yellow
|
|
219
222
|
else
|
|
220
|
-
|
|
223
|
+
:green
|
|
221
224
|
end
|
|
225
|
+
colored_delta = pastel.decorate(delta_str, color_style, :dim)
|
|
222
226
|
token_info << colored_delta
|
|
223
227
|
|
|
224
228
|
# Cache status indicator (using theme)
|
|
@@ -226,31 +230,44 @@ module Clacky
|
|
|
226
230
|
cache_read = token_data[:cache_read]
|
|
227
231
|
cache_used = cache_read > 0 || cache_write > 0
|
|
228
232
|
if cache_used
|
|
229
|
-
token_info << theme.
|
|
233
|
+
token_info << pastel.dim(theme.symbol(:cached))
|
|
230
234
|
end
|
|
231
235
|
|
|
232
236
|
# Input tokens (with cache breakdown if available)
|
|
233
237
|
prompt_tokens = token_data[:prompt_tokens]
|
|
234
238
|
if cache_write > 0 || cache_read > 0
|
|
235
239
|
input_detail = "#{prompt_tokens} (cache: #{cache_read} read, #{cache_write} write)"
|
|
236
|
-
token_info << "Input: #{input_detail}"
|
|
240
|
+
token_info << pastel.dim("Input: #{input_detail}")
|
|
237
241
|
else
|
|
238
|
-
token_info << "Input: #{prompt_tokens}"
|
|
242
|
+
token_info << pastel.dim("Input: #{prompt_tokens}")
|
|
239
243
|
end
|
|
240
244
|
|
|
241
245
|
# Output tokens
|
|
242
|
-
token_info << "Output: #{token_data[:completion_tokens]}"
|
|
246
|
+
token_info << pastel.dim("Output: #{token_data[:completion_tokens]}")
|
|
243
247
|
|
|
244
248
|
# Total
|
|
245
|
-
token_info << "Total: #{token_data[:total_tokens]}"
|
|
249
|
+
token_info << pastel.dim("Total: #{token_data[:total_tokens]}")
|
|
246
250
|
|
|
247
|
-
# Cost for this iteration
|
|
251
|
+
# Cost for this iteration with color coding (red/yellow for high cost, dim for normal)
|
|
248
252
|
if token_data[:cost]
|
|
249
|
-
|
|
253
|
+
cost = token_data[:cost]
|
|
254
|
+
cost_value = "$#{cost.round(6)}"
|
|
255
|
+
if cost >= 0.1
|
|
256
|
+
# High cost - red warning
|
|
257
|
+
colored_cost = pastel.decorate(cost_value, :red, :dim)
|
|
258
|
+
token_info << pastel.dim("Cost: ") + colored_cost
|
|
259
|
+
elsif cost >= 0.05
|
|
260
|
+
# Medium cost - yellow warning
|
|
261
|
+
colored_cost = pastel.decorate(cost_value, :yellow, :dim)
|
|
262
|
+
token_info << pastel.dim("Cost: ") + colored_cost
|
|
263
|
+
else
|
|
264
|
+
# Low cost - normal gray
|
|
265
|
+
token_info << pastel.dim("Cost: #{cost_value}")
|
|
266
|
+
end
|
|
250
267
|
end
|
|
251
268
|
|
|
252
|
-
# Display through output system
|
|
253
|
-
token_display =
|
|
269
|
+
# Display through output system (already all dimmed, just add prefix)
|
|
270
|
+
token_display = pastel.dim(" [Tokens] ") + token_info.join(pastel.dim(' | '))
|
|
254
271
|
append_output(token_display)
|
|
255
272
|
end
|
|
256
273
|
|
|
@@ -308,10 +325,29 @@ module Clacky
|
|
|
308
325
|
# Show assistant message
|
|
309
326
|
# @param content [String] Message content
|
|
310
327
|
def show_assistant_message(content)
|
|
311
|
-
|
|
328
|
+
# Filter out thinking tags from models like MiniMax M2.1 that use <think>...</think>
|
|
329
|
+
filtered_content = filter_thinking_tags(content)
|
|
330
|
+
return if filtered_content.nil? || filtered_content.strip.empty?
|
|
331
|
+
|
|
332
|
+
output = @renderer.render_assistant_message(filtered_content)
|
|
312
333
|
append_output(output)
|
|
313
334
|
end
|
|
314
335
|
|
|
336
|
+
# Filter out thinking tags from content
|
|
337
|
+
# Some models (e.g., MiniMax M2.1) wrap their reasoning in <think>...</think> tags
|
|
338
|
+
# @param content [String] Raw content from model
|
|
339
|
+
# @return [String] Content with thinking tags removed
|
|
340
|
+
def filter_thinking_tags(content)
|
|
341
|
+
return content if content.nil?
|
|
342
|
+
|
|
343
|
+
# Remove <think>...</think> blocks (multiline, case-insensitive)
|
|
344
|
+
# Also handles variations like <thinking>...</thinking>
|
|
345
|
+
filtered = content.gsub(%r{<think(?:ing)?>\s*.*?\s*</think(?:ing)?>}mi, '')
|
|
346
|
+
|
|
347
|
+
# Clean up extra whitespace left behind
|
|
348
|
+
filtered.gsub(/\n{3,}/, "\n\n").strip
|
|
349
|
+
end
|
|
350
|
+
|
|
315
351
|
# Show tool call
|
|
316
352
|
# @param name [String] Tool name
|
|
317
353
|
# @param args [String, Hash] Tool arguments (JSON string or Hash)
|
|
@@ -344,6 +380,10 @@ module Clacky
|
|
|
344
380
|
def show_complete(iterations:, cost:, duration: nil, cache_stats: nil)
|
|
345
381
|
# Update status back to 'idle' when task is complete
|
|
346
382
|
update_sessionbar(status: 'idle')
|
|
383
|
+
|
|
384
|
+
# Clear user tip when agent stops working
|
|
385
|
+
@input_area.clear_user_tip
|
|
386
|
+
@layout.render_input
|
|
347
387
|
|
|
348
388
|
# Only show completion message for complex tasks (>5 iterations)
|
|
349
389
|
return if iterations <= 5
|
|
@@ -351,7 +391,7 @@ module Clacky
|
|
|
351
391
|
cache_tokens = cache_stats&.dig(:cache_read_input_tokens)
|
|
352
392
|
cache_requests = cache_stats&.dig(:total_requests)
|
|
353
393
|
cache_hits = cache_stats&.dig(:cache_hit_requests)
|
|
354
|
-
|
|
394
|
+
|
|
355
395
|
output = @renderer.render_task_complete(
|
|
356
396
|
iterations: iterations,
|
|
357
397
|
cost: cost,
|
|
@@ -365,7 +405,8 @@ module Clacky
|
|
|
365
405
|
|
|
366
406
|
# Show progress indicator with dynamic elapsed time
|
|
367
407
|
# @param message [String] Progress message (optional, will use random thinking verb if nil)
|
|
368
|
-
|
|
408
|
+
# @param prefix_newline [Boolean] Whether to add a blank line before progress (default: true)
|
|
409
|
+
def show_progress(message = nil, prefix_newline: true)
|
|
369
410
|
# Stop any existing progress thread
|
|
370
411
|
stop_progress_thread
|
|
371
412
|
|
|
@@ -375,8 +416,9 @@ module Clacky
|
|
|
375
416
|
@progress_message = message || Clacky::THINKING_VERBS.sample
|
|
376
417
|
@progress_start_time = Time.now
|
|
377
418
|
|
|
378
|
-
# Show initial progress
|
|
379
|
-
|
|
419
|
+
# Show initial progress (yellow, active)
|
|
420
|
+
append_output("") if prefix_newline
|
|
421
|
+
output = @renderer.render_working("#{@progress_message}… (ctrl+c to interrupt)")
|
|
380
422
|
append_output(output)
|
|
381
423
|
|
|
382
424
|
# Start background thread to update elapsed time
|
|
@@ -386,7 +428,7 @@ module Clacky
|
|
|
386
428
|
next unless @progress_start_time
|
|
387
429
|
|
|
388
430
|
elapsed = (Time.now - @progress_start_time).to_i
|
|
389
|
-
update_progress_line(@renderer.
|
|
431
|
+
update_progress_line(@renderer.render_working("#{@progress_message}… (ctrl+c to interrupt · #{elapsed}s)"))
|
|
390
432
|
end
|
|
391
433
|
rescue => e
|
|
392
434
|
# Silently handle thread errors
|
|
@@ -395,8 +437,19 @@ module Clacky
|
|
|
395
437
|
|
|
396
438
|
# Clear progress indicator
|
|
397
439
|
def clear_progress
|
|
440
|
+
# Calculate elapsed time before stopping
|
|
441
|
+
elapsed_time = @progress_start_time ? (Time.now - @progress_start_time).to_i : 0
|
|
442
|
+
|
|
443
|
+
# Stop the progress thread
|
|
398
444
|
stop_progress_thread
|
|
399
|
-
|
|
445
|
+
|
|
446
|
+
# Update the final progress line to gray (stopped state)
|
|
447
|
+
if @progress_message && elapsed_time > 0
|
|
448
|
+
final_output = @renderer.render_progress("#{@progress_message}… (#{elapsed_time}s)")
|
|
449
|
+
update_progress_line(final_output)
|
|
450
|
+
else
|
|
451
|
+
clear_progress_line
|
|
452
|
+
end
|
|
400
453
|
end
|
|
401
454
|
|
|
402
455
|
# Stop the progress update thread
|
|
@@ -410,8 +463,9 @@ module Clacky
|
|
|
410
463
|
|
|
411
464
|
# Show info message
|
|
412
465
|
# @param message [String] Info message
|
|
413
|
-
|
|
414
|
-
|
|
466
|
+
# @param prefix_newline [Boolean] Whether to add newline before message (default: true)
|
|
467
|
+
def show_info(message, prefix_newline: true)
|
|
468
|
+
output = @renderer.render_system_message(message, prefix_newline: prefix_newline)
|
|
415
469
|
append_output(output)
|
|
416
470
|
end
|
|
417
471
|
|
|
@@ -432,11 +486,17 @@ module Clacky
|
|
|
432
486
|
# Set workspace status to idle (called when agent stops working)
|
|
433
487
|
def set_idle_status
|
|
434
488
|
update_sessionbar(status: 'idle')
|
|
489
|
+
# Clear user tip when agent stops working
|
|
490
|
+
@input_area.clear_user_tip
|
|
491
|
+
@layout.render_input
|
|
435
492
|
end
|
|
436
493
|
|
|
437
494
|
# Set workspace status to working (called when agent starts working)
|
|
438
495
|
def set_working_status
|
|
439
496
|
update_sessionbar(status: 'working')
|
|
497
|
+
# Show a random user tip with 40% probability when agent starts working
|
|
498
|
+
@input_area.show_user_tip(probability: 0.4)
|
|
499
|
+
@layout.render_input
|
|
440
500
|
end
|
|
441
501
|
|
|
442
502
|
# Show help text
|
|
@@ -484,7 +544,7 @@ module Clacky
|
|
|
484
544
|
|
|
485
545
|
# Create InlineInput with styled prompt
|
|
486
546
|
inline_input = Components::InlineInput.new(
|
|
487
|
-
prompt:
|
|
547
|
+
prompt: " Press Enter to approve, 'n' to reject, or provide feedback: ",
|
|
488
548
|
default: nil
|
|
489
549
|
)
|
|
490
550
|
@inline_input = inline_input
|
|
@@ -535,9 +595,38 @@ module Clacky
|
|
|
535
595
|
|
|
536
596
|
diff = Diffy::Diff.new(old_content, new_content, context: 3)
|
|
537
597
|
all_lines = diff.to_s(:color).lines
|
|
538
|
-
|
|
598
|
+
plain_lines = diff.to_s.lines
|
|
599
|
+
|
|
600
|
+
# Add line numbers to diff output
|
|
601
|
+
old_line_num = 0
|
|
602
|
+
new_line_num = 0
|
|
603
|
+
|
|
604
|
+
numbered_lines = all_lines.each_with_index.map do |line, index|
|
|
605
|
+
# Use plain text to detect line type (remove ANSI codes)
|
|
606
|
+
plain_line = plain_lines[index]&.chomp || line.gsub(/\e\[[0-9;]*m/, '').chomp
|
|
607
|
+
|
|
608
|
+
# Remove trailing newline from colored line to avoid double newlines
|
|
609
|
+
colored_line = line.chomp
|
|
610
|
+
|
|
611
|
+
# Determine line type and number (use single line number for simplicity)
|
|
612
|
+
if plain_line.start_with?('+') || plain_line.start_with?('-') || plain_line.start_with?(' ')
|
|
613
|
+
new_line_num += 1
|
|
614
|
+
sprintf("%4d | %s", new_line_num, colored_line)
|
|
615
|
+
elsif plain_line.start_with?('@@')
|
|
616
|
+
# Diff header: extract line numbers from @@ -old_start,old_count +new_start,new_count @@
|
|
617
|
+
if plain_line =~ /@@ -(\d+)(?:,\d+)? (\d+)(?:,\d+)? @@/
|
|
618
|
+
new_line_num = $2.to_i - 1
|
|
619
|
+
end
|
|
620
|
+
sprintf("%4s | %s", "", colored_line)
|
|
621
|
+
else
|
|
622
|
+
# Other lines (headers, etc.)
|
|
623
|
+
sprintf("%4s | %s", "", colored_line)
|
|
624
|
+
end
|
|
625
|
+
end
|
|
626
|
+
|
|
627
|
+
display_lines = numbered_lines.first(max_lines)
|
|
628
|
+
display_lines.each { |line| append_output(line) }
|
|
539
629
|
|
|
540
|
-
display_lines.each { |line| append_output(line.chomp) }
|
|
541
630
|
if all_lines.size > max_lines
|
|
542
631
|
append_output("\n... (#{all_lines.size - max_lines} more lines, diff truncated)")
|
|
543
632
|
end
|
|
@@ -599,6 +688,35 @@ module Clacky
|
|
|
599
688
|
append_output(content)
|
|
600
689
|
end
|
|
601
690
|
|
|
691
|
+
# Display recent user messages when loading session
|
|
692
|
+
# @param user_messages [Array<String>] Array of recent user message texts
|
|
693
|
+
def display_session_history(user_messages)
|
|
694
|
+
theme = ThemeManager.current_theme
|
|
695
|
+
|
|
696
|
+
# Show logo banner only
|
|
697
|
+
append_output(@welcome_banner.render_logo)
|
|
698
|
+
|
|
699
|
+
# Show simple header
|
|
700
|
+
append_output(theme.format_text("Recent conversation:", :info))
|
|
701
|
+
|
|
702
|
+
# Display each user message with numbering
|
|
703
|
+
user_messages.each_with_index do |msg, index|
|
|
704
|
+
# Truncate long messages
|
|
705
|
+
display_msg = if msg.length > 140
|
|
706
|
+
"#{msg[0..137]}..."
|
|
707
|
+
else
|
|
708
|
+
msg
|
|
709
|
+
end
|
|
710
|
+
|
|
711
|
+
# Show with number and indentation
|
|
712
|
+
append_output(" #{index + 1}. #{display_msg}")
|
|
713
|
+
end
|
|
714
|
+
|
|
715
|
+
# Bottom spacing and continuation prompt
|
|
716
|
+
append_output("")
|
|
717
|
+
append_output(theme.format_text("Session restored. Feel free to continue with your next task.", :success))
|
|
718
|
+
end
|
|
719
|
+
|
|
602
720
|
# Main input loop
|
|
603
721
|
def input_loop
|
|
604
722
|
@layout.screen.enable_raw_mode
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
require_relative "components/message_component"
|
|
4
4
|
require_relative "components/tool_component"
|
|
5
5
|
require_relative "components/common_component"
|
|
6
|
+
require_relative "markdown_renderer"
|
|
6
7
|
|
|
7
8
|
module Clacky
|
|
8
9
|
module UI2
|
|
@@ -31,9 +32,16 @@ module Clacky
|
|
|
31
32
|
# @param timestamp [Time, nil] Optional timestamp
|
|
32
33
|
# @return [String] Rendered message
|
|
33
34
|
def render_assistant_message(content, timestamp: nil)
|
|
35
|
+
# Render markdown if content contains markdown syntax
|
|
36
|
+
rendered_content = if MarkdownRenderer.markdown?(content)
|
|
37
|
+
MarkdownRenderer.render(content)
|
|
38
|
+
else
|
|
39
|
+
content
|
|
40
|
+
end
|
|
41
|
+
|
|
34
42
|
@message_component.render(
|
|
35
43
|
role: "assistant",
|
|
36
|
-
content:
|
|
44
|
+
content: rendered_content,
|
|
37
45
|
timestamp: timestamp
|
|
38
46
|
)
|
|
39
47
|
end
|
|
@@ -41,12 +49,14 @@ module Clacky
|
|
|
41
49
|
# Render a system message
|
|
42
50
|
# @param content [String] Message content
|
|
43
51
|
# @param timestamp [Time, nil] Optional timestamp
|
|
52
|
+
# @param prefix_newline [Boolean] Whether to add newline before message
|
|
44
53
|
# @return [String] Rendered message
|
|
45
|
-
def render_system_message(content, timestamp: nil)
|
|
54
|
+
def render_system_message(content, timestamp: nil, prefix_newline: true)
|
|
46
55
|
@message_component.render(
|
|
47
56
|
role: "system",
|
|
48
57
|
content: content,
|
|
49
|
-
timestamp: timestamp
|
|
58
|
+
timestamp: timestamp,
|
|
59
|
+
prefix_newline: prefix_newline
|
|
50
60
|
)
|
|
51
61
|
end
|
|
52
62
|
|
|
@@ -108,13 +118,20 @@ module Clacky
|
|
|
108
118
|
@common_component.render_thinking
|
|
109
119
|
end
|
|
110
120
|
|
|
111
|
-
# Render progress message
|
|
121
|
+
# Render progress message (stopped state, gray)
|
|
112
122
|
# @param message [String] Progress message
|
|
113
123
|
# @return [String] Progress indicator
|
|
114
124
|
def render_progress(message)
|
|
115
125
|
@common_component.render_progress(message)
|
|
116
126
|
end
|
|
117
127
|
|
|
128
|
+
# Render working message (active state, yellow)
|
|
129
|
+
# @param message [String] Progress message
|
|
130
|
+
# @return [String] Working indicator
|
|
131
|
+
def render_working(message)
|
|
132
|
+
@common_component.render_working(message)
|
|
133
|
+
end
|
|
134
|
+
|
|
118
135
|
# Render success message
|
|
119
136
|
# @param message [String] Success message
|
|
120
137
|
# @return [String] Success message
|
data/lib/clacky/ui2.rb
CHANGED
|
@@ -10,7 +10,6 @@ require_relative "ui2/view_renderer"
|
|
|
10
10
|
require_relative "ui2/ui_controller"
|
|
11
11
|
|
|
12
12
|
require_relative "ui2/components/base_component"
|
|
13
|
-
require_relative "ui2/components/output_area"
|
|
14
13
|
require_relative "ui2/components/input_area"
|
|
15
14
|
require_relative "ui2/components/message_component"
|
|
16
15
|
require_relative "ui2/components/tool_component"
|
|
@@ -44,10 +44,11 @@ module Clacky
|
|
|
44
44
|
result
|
|
45
45
|
end
|
|
46
46
|
|
|
47
|
-
# Validate required parameters
|
|
47
|
+
# Validate required parameters and filter unknown parameters
|
|
48
48
|
def self.validate_required_params(call, args, tool_registry)
|
|
49
49
|
tool = tool_registry.get(call[:name])
|
|
50
50
|
required = tool.parameters&.dig(:required) || []
|
|
51
|
+
properties = tool.parameters&.dig(:properties) || {}
|
|
51
52
|
|
|
52
53
|
missing = required.reject { |param|
|
|
53
54
|
args.key?(param.to_sym) || args.key?(param.to_s)
|
|
@@ -57,7 +58,11 @@ module Clacky
|
|
|
57
58
|
raise MissingRequiredParamsError.new(call[:name], missing, args.keys)
|
|
58
59
|
end
|
|
59
60
|
|
|
60
|
-
|
|
61
|
+
# Filter out unknown parameters to prevent errors when LLM sends extra arguments
|
|
62
|
+
known_params = properties.keys.map(&:to_sym) + properties.keys.map(&:to_s)
|
|
63
|
+
filtered_args = args.select { |key, _| known_params.include?(key) }
|
|
64
|
+
|
|
65
|
+
filtered_args
|
|
61
66
|
end
|
|
62
67
|
|
|
63
68
|
# Generate error message with tool definition
|