openclacky 0.6.0 → 0.6.1
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 +28 -0
- data/lib/clacky/agent.rb +137 -12
- data/lib/clacky/cli.rb +15 -5
- 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 +101 -8
- data/lib/clacky/tools/shell.rb +2 -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 +124 -29
- data/lib/clacky/ui2/components/message_component.rb +7 -3
- data/lib/clacky/ui2/components/output_area.rb +1 -1
- 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 +120 -14
- 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 +2 -0
- data/lib/clacky/ui2/themes/minimal_theme.rb +2 -0
- data/lib/clacky/ui2/ui_controller.rb +70 -12
- data/lib/clacky/ui2/view_renderer.rb +21 -4
- data/lib/clacky/version.rb +1 -1
- metadata +16 -1
|
@@ -177,26 +177,40 @@ module Clacky
|
|
|
177
177
|
# Track current row, scroll when reaching fixed area
|
|
178
178
|
# @param content [String] Content to append
|
|
179
179
|
def append_output(content)
|
|
180
|
-
return if content.nil?
|
|
180
|
+
return if content.nil?
|
|
181
181
|
|
|
182
182
|
@render_mutex.synchronize do
|
|
183
183
|
max_output_row = fixed_area_start_row - 1
|
|
184
184
|
|
|
185
|
+
# Special handling for empty string - just add a blank line
|
|
186
|
+
if content.empty?
|
|
187
|
+
print "\n"
|
|
188
|
+
@output_row += 1
|
|
189
|
+
render_fixed_areas
|
|
190
|
+
screen.flush
|
|
191
|
+
return
|
|
192
|
+
end
|
|
193
|
+
|
|
185
194
|
content.split("\n").each do |line|
|
|
186
|
-
#
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
195
|
+
# Wrap long lines to prevent display issues
|
|
196
|
+
wrapped_lines = wrap_long_line(line)
|
|
197
|
+
|
|
198
|
+
wrapped_lines.each do |wrapped_line|
|
|
199
|
+
# If at max row, need to scroll before outputting
|
|
200
|
+
if @output_row > max_output_row
|
|
201
|
+
# Move to bottom of screen and print newline to trigger scroll
|
|
202
|
+
screen.move_cursor(screen.height - 1, 0)
|
|
203
|
+
print "\n"
|
|
204
|
+
# Stay at max_output_row for next output
|
|
205
|
+
@output_row = max_output_row
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Output line at current position
|
|
209
|
+
screen.move_cursor(@output_row, 0)
|
|
210
|
+
screen.clear_line
|
|
211
|
+
output_area.append(wrapped_line)
|
|
212
|
+
@output_row += 1
|
|
193
213
|
end
|
|
194
|
-
|
|
195
|
-
# Output line at current position
|
|
196
|
-
screen.move_cursor(@output_row, 0)
|
|
197
|
-
screen.clear_line
|
|
198
|
-
output_area.append(line)
|
|
199
|
-
@output_row += 1
|
|
200
214
|
end
|
|
201
215
|
|
|
202
216
|
# Re-render fixed areas at screen bottom
|
|
@@ -268,6 +282,98 @@ module Clacky
|
|
|
268
282
|
|
|
269
283
|
private
|
|
270
284
|
|
|
285
|
+
# Wrap a long line into multiple lines based on terminal width
|
|
286
|
+
# Considers display width of multi-byte characters (e.g., Chinese characters)
|
|
287
|
+
# @param line [String] Line to wrap
|
|
288
|
+
# @return [Array<String>] Array of wrapped lines
|
|
289
|
+
def wrap_long_line(line)
|
|
290
|
+
return [""] if line.nil? || line.empty?
|
|
291
|
+
|
|
292
|
+
max_width = screen.width
|
|
293
|
+
return [line] if max_width <= 0
|
|
294
|
+
|
|
295
|
+
# Strip ANSI codes for width calculation
|
|
296
|
+
visible_line = line.gsub(/\e\[[0-9;]*m/, '')
|
|
297
|
+
|
|
298
|
+
# Check if line needs wrapping
|
|
299
|
+
display_width = calculate_display_width(visible_line)
|
|
300
|
+
return [line] if display_width <= max_width
|
|
301
|
+
|
|
302
|
+
# Line needs wrapping - split by considering display width
|
|
303
|
+
wrapped = []
|
|
304
|
+
current_line = ""
|
|
305
|
+
current_width = 0
|
|
306
|
+
ansi_codes = [] # Track ANSI codes to carry over
|
|
307
|
+
|
|
308
|
+
# Extract ANSI codes and text segments
|
|
309
|
+
segments = line.split(/(\e\[[0-9;]*m)/)
|
|
310
|
+
|
|
311
|
+
segments.each do |segment|
|
|
312
|
+
if segment =~ /^\e\[[0-9;]*m$/
|
|
313
|
+
# ANSI code - add to current codes
|
|
314
|
+
ansi_codes << segment
|
|
315
|
+
current_line += segment
|
|
316
|
+
else
|
|
317
|
+
# Text segment - process character by character
|
|
318
|
+
segment.each_char do |char|
|
|
319
|
+
char_width = char_display_width(char)
|
|
320
|
+
|
|
321
|
+
if current_width + char_width > max_width && !current_line.empty?
|
|
322
|
+
# Complete current line
|
|
323
|
+
wrapped << current_line
|
|
324
|
+
# Start new line with carried-over ANSI codes
|
|
325
|
+
current_line = ansi_codes.join
|
|
326
|
+
current_width = 0
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
current_line += char
|
|
330
|
+
current_width += char_width
|
|
331
|
+
end
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
# Add remaining content
|
|
336
|
+
wrapped << current_line unless current_line.empty? || current_line == ansi_codes.join
|
|
337
|
+
|
|
338
|
+
wrapped.empty? ? [""] : wrapped
|
|
339
|
+
end
|
|
340
|
+
|
|
341
|
+
# Calculate display width of a single character
|
|
342
|
+
# @param char [String] Single character
|
|
343
|
+
# @return [Integer] Display width (1 or 2)
|
|
344
|
+
def char_display_width(char)
|
|
345
|
+
code = char.ord
|
|
346
|
+
# East Asian Wide and Fullwidth characters take 2 columns
|
|
347
|
+
if (code >= 0x1100 && code <= 0x115F) ||
|
|
348
|
+
(code >= 0x2329 && code <= 0x232A) ||
|
|
349
|
+
(code >= 0x2E80 && code <= 0x303E) ||
|
|
350
|
+
(code >= 0x3040 && code <= 0xA4CF) ||
|
|
351
|
+
(code >= 0xAC00 && code <= 0xD7A3) ||
|
|
352
|
+
(code >= 0xF900 && code <= 0xFAFF) ||
|
|
353
|
+
(code >= 0xFE10 && code <= 0xFE19) ||
|
|
354
|
+
(code >= 0xFE30 && code <= 0xFE6F) ||
|
|
355
|
+
(code >= 0xFF00 && code <= 0xFF60) ||
|
|
356
|
+
(code >= 0xFFE0 && code <= 0xFFE6) ||
|
|
357
|
+
(code >= 0x1F300 && code <= 0x1F9FF) ||
|
|
358
|
+
(code >= 0x20000 && code <= 0x2FFFD) ||
|
|
359
|
+
(code >= 0x30000 && code <= 0x3FFFD)
|
|
360
|
+
2
|
|
361
|
+
else
|
|
362
|
+
1
|
|
363
|
+
end
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
# Calculate display width of a string (considering multi-byte characters)
|
|
367
|
+
# @param text [String] Text to calculate
|
|
368
|
+
# @return [Integer] Display width
|
|
369
|
+
def calculate_display_width(text)
|
|
370
|
+
width = 0
|
|
371
|
+
text.each_char do |char|
|
|
372
|
+
width += char_display_width(char)
|
|
373
|
+
end
|
|
374
|
+
width
|
|
375
|
+
end
|
|
376
|
+
|
|
271
377
|
# Calculate fixed area height (gap + todo + input)
|
|
272
378
|
def fixed_area_height
|
|
273
379
|
todo_height = @todo_area&.height || 0
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "tty-markdown"
|
|
4
|
+
require_relative "theme_manager"
|
|
5
|
+
|
|
6
|
+
module Clacky
|
|
7
|
+
module UI2
|
|
8
|
+
# MarkdownRenderer handles rendering Markdown content with syntax highlighting
|
|
9
|
+
module MarkdownRenderer
|
|
10
|
+
class << self
|
|
11
|
+
# Render markdown content with theme-aware colors
|
|
12
|
+
# @param content [String] Markdown content to render
|
|
13
|
+
# @return [String] Rendered content with ANSI colors
|
|
14
|
+
def render(content)
|
|
15
|
+
return content if content.nil? || content.empty?
|
|
16
|
+
|
|
17
|
+
# Get current theme colors
|
|
18
|
+
theme = ThemeManager.current_theme
|
|
19
|
+
|
|
20
|
+
# Configure tty-markdown colors based on current theme
|
|
21
|
+
# tty-markdown uses Pastel internally, we can configure symbols
|
|
22
|
+
parsed = TTY::Markdown.parse(content,
|
|
23
|
+
colors: theme_colors,
|
|
24
|
+
width: TTY::Screen.width - 4 # Leave some margin
|
|
25
|
+
)
|
|
26
|
+
|
|
27
|
+
parsed
|
|
28
|
+
rescue StandardError => e
|
|
29
|
+
# Fallback to plain content if rendering fails
|
|
30
|
+
content
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Check if content looks like markdown
|
|
34
|
+
# @param content [String] Content to check
|
|
35
|
+
# @return [Boolean] true if content appears to be markdown
|
|
36
|
+
def markdown?(content)
|
|
37
|
+
return false if content.nil? || content.empty?
|
|
38
|
+
|
|
39
|
+
# Check for common markdown patterns
|
|
40
|
+
content.match?(/^#+ /) || # Headers
|
|
41
|
+
content.match?(/```/) || # Code blocks
|
|
42
|
+
content.match?(/^\s*[-*+] /) || # Unordered lists
|
|
43
|
+
content.match?(/^\s*\d+\. /) || # Ordered lists
|
|
44
|
+
content.match?(/\[.+\]\(.+\)/) || # Links
|
|
45
|
+
content.match?(/^\s*> /) || # Blockquotes
|
|
46
|
+
content.match?(/\*\*.+\*\*/) || # Bold
|
|
47
|
+
content.match?(/`.+`/) || # Inline code
|
|
48
|
+
content.match?(/^\s*\|.+\|/) || # Tables
|
|
49
|
+
content.match?(/^---+$/) # Horizontal rules
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
private
|
|
53
|
+
|
|
54
|
+
# Get theme-aware colors for markdown rendering
|
|
55
|
+
# @return [Hash] Color configuration for tty-markdown
|
|
56
|
+
def theme_colors
|
|
57
|
+
theme = ThemeManager.current_theme
|
|
58
|
+
|
|
59
|
+
# Map our theme colors to tty-markdown's expected format
|
|
60
|
+
{
|
|
61
|
+
# Headers use info color (cyan/blue)
|
|
62
|
+
header: theme.colors[:info],
|
|
63
|
+
# Code blocks use dim color
|
|
64
|
+
code: theme.colors[:thinking],
|
|
65
|
+
# Links use success color (green)
|
|
66
|
+
link: theme.colors[:success],
|
|
67
|
+
# Lists use default text color
|
|
68
|
+
list: :bright_white,
|
|
69
|
+
# Strong/bold use bright white
|
|
70
|
+
strong: :bright_white,
|
|
71
|
+
# Emphasis/italic use white
|
|
72
|
+
em: :white,
|
|
73
|
+
# Note/blockquote use dim color
|
|
74
|
+
note: theme.colors[:thinking],
|
|
75
|
+
}
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
@@ -171,14 +171,33 @@ module Clacky
|
|
|
171
171
|
buffer.force_encoding('UTF-8')
|
|
172
172
|
|
|
173
173
|
# Keep reading available characters
|
|
174
|
+
loop_count = 0
|
|
175
|
+
empty_checks = 0
|
|
176
|
+
|
|
174
177
|
loop do
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
178
|
+
# Check if there's data available immediately
|
|
179
|
+
has_data = IO.select([$stdin], nil, nil, 0)
|
|
180
|
+
|
|
181
|
+
if has_data
|
|
182
|
+
next_char = $stdin.getc
|
|
183
|
+
break unless next_char
|
|
184
|
+
|
|
185
|
+
next_char = next_char.force_encoding('UTF-8') if next_char.encoding != Encoding::UTF_8
|
|
186
|
+
buffer << next_char
|
|
187
|
+
loop_count += 1
|
|
188
|
+
empty_checks = 0 # Reset empty check counter
|
|
189
|
+
else
|
|
190
|
+
# No immediate data, but wait a bit to see if more is coming
|
|
191
|
+
# This handles the case where paste data arrives in chunks
|
|
192
|
+
empty_checks += 1
|
|
193
|
+
if empty_checks == 1
|
|
194
|
+
# First empty check - wait 10ms for more data
|
|
195
|
+
sleep 0.01
|
|
196
|
+
else
|
|
197
|
+
# Second empty check - really no more data
|
|
198
|
+
break
|
|
199
|
+
end
|
|
200
|
+
end
|
|
182
201
|
end
|
|
183
202
|
|
|
184
203
|
# If we buffered multiple characters or newlines, treat as rapid input (paste)
|
|
@@ -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: "[!!]",
|
|
@@ -36,6 +37,7 @@ module Clacky
|
|
|
36
37
|
tool_planned: [:bright_blue, :blue],
|
|
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: "!",
|
|
@@ -33,6 +34,7 @@ module Clacky
|
|
|
33
34
|
tool_planned: [:blue, :blue],
|
|
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],
|
|
@@ -59,7 +59,8 @@ module Clacky
|
|
|
59
59
|
end
|
|
60
60
|
|
|
61
61
|
# Initialize screen and show banner (separate from input loop)
|
|
62
|
-
|
|
62
|
+
# @param recent_user_messages [Array<String>, nil] Recent user messages when loading session
|
|
63
|
+
def initialize_and_show_banner(recent_user_messages: nil)
|
|
63
64
|
@running = true
|
|
64
65
|
|
|
65
66
|
# Set session bar data before initializing screen
|
|
@@ -73,8 +74,12 @@ module Clacky
|
|
|
73
74
|
|
|
74
75
|
@layout.initialize_screen
|
|
75
76
|
|
|
76
|
-
# Display welcome banner
|
|
77
|
-
|
|
77
|
+
# Display welcome banner or session history
|
|
78
|
+
if recent_user_messages && !recent_user_messages.empty?
|
|
79
|
+
display_session_history(recent_user_messages)
|
|
80
|
+
else
|
|
81
|
+
display_welcome_banner
|
|
82
|
+
end
|
|
78
83
|
end
|
|
79
84
|
|
|
80
85
|
# Start input loop (separate from initialization)
|
|
@@ -344,6 +349,10 @@ module Clacky
|
|
|
344
349
|
def show_complete(iterations:, cost:, duration: nil, cache_stats: nil)
|
|
345
350
|
# Update status back to 'idle' when task is complete
|
|
346
351
|
update_sessionbar(status: 'idle')
|
|
352
|
+
|
|
353
|
+
# Clear user tip when agent stops working
|
|
354
|
+
@input_area.clear_user_tip
|
|
355
|
+
@layout.render_input
|
|
347
356
|
|
|
348
357
|
# Only show completion message for complex tasks (>5 iterations)
|
|
349
358
|
return if iterations <= 5
|
|
@@ -351,7 +360,7 @@ module Clacky
|
|
|
351
360
|
cache_tokens = cache_stats&.dig(:cache_read_input_tokens)
|
|
352
361
|
cache_requests = cache_stats&.dig(:total_requests)
|
|
353
362
|
cache_hits = cache_stats&.dig(:cache_hit_requests)
|
|
354
|
-
|
|
363
|
+
|
|
355
364
|
output = @renderer.render_task_complete(
|
|
356
365
|
iterations: iterations,
|
|
357
366
|
cost: cost,
|
|
@@ -365,7 +374,8 @@ module Clacky
|
|
|
365
374
|
|
|
366
375
|
# Show progress indicator with dynamic elapsed time
|
|
367
376
|
# @param message [String] Progress message (optional, will use random thinking verb if nil)
|
|
368
|
-
|
|
377
|
+
# @param prefix_newline [Boolean] Whether to add a blank line before progress (default: true)
|
|
378
|
+
def show_progress(message = nil, prefix_newline: true)
|
|
369
379
|
# Stop any existing progress thread
|
|
370
380
|
stop_progress_thread
|
|
371
381
|
|
|
@@ -375,8 +385,9 @@ module Clacky
|
|
|
375
385
|
@progress_message = message || Clacky::THINKING_VERBS.sample
|
|
376
386
|
@progress_start_time = Time.now
|
|
377
387
|
|
|
378
|
-
# Show initial progress
|
|
379
|
-
|
|
388
|
+
# Show initial progress (yellow, active)
|
|
389
|
+
append_output("") if prefix_newline
|
|
390
|
+
output = @renderer.render_working("#{@progress_message}… (ctrl+c to interrupt)")
|
|
380
391
|
append_output(output)
|
|
381
392
|
|
|
382
393
|
# Start background thread to update elapsed time
|
|
@@ -386,7 +397,7 @@ module Clacky
|
|
|
386
397
|
next unless @progress_start_time
|
|
387
398
|
|
|
388
399
|
elapsed = (Time.now - @progress_start_time).to_i
|
|
389
|
-
update_progress_line(@renderer.
|
|
400
|
+
update_progress_line(@renderer.render_working("#{@progress_message}… (ctrl+c to interrupt · #{elapsed}s)"))
|
|
390
401
|
end
|
|
391
402
|
rescue => e
|
|
392
403
|
# Silently handle thread errors
|
|
@@ -395,8 +406,19 @@ module Clacky
|
|
|
395
406
|
|
|
396
407
|
# Clear progress indicator
|
|
397
408
|
def clear_progress
|
|
409
|
+
# Calculate elapsed time before stopping
|
|
410
|
+
elapsed_time = @progress_start_time ? (Time.now - @progress_start_time).to_i : 0
|
|
411
|
+
|
|
412
|
+
# Stop the progress thread
|
|
398
413
|
stop_progress_thread
|
|
399
|
-
|
|
414
|
+
|
|
415
|
+
# Update the final progress line to gray (stopped state)
|
|
416
|
+
if @progress_message && elapsed_time > 0
|
|
417
|
+
final_output = @renderer.render_progress("#{@progress_message}… (#{elapsed_time}s)")
|
|
418
|
+
update_progress_line(final_output)
|
|
419
|
+
else
|
|
420
|
+
clear_progress_line
|
|
421
|
+
end
|
|
400
422
|
end
|
|
401
423
|
|
|
402
424
|
# Stop the progress update thread
|
|
@@ -410,8 +432,9 @@ module Clacky
|
|
|
410
432
|
|
|
411
433
|
# Show info message
|
|
412
434
|
# @param message [String] Info message
|
|
413
|
-
|
|
414
|
-
|
|
435
|
+
# @param prefix_newline [Boolean] Whether to add newline before message (default: true)
|
|
436
|
+
def show_info(message, prefix_newline: true)
|
|
437
|
+
output = @renderer.render_system_message(message, prefix_newline: prefix_newline)
|
|
415
438
|
append_output(output)
|
|
416
439
|
end
|
|
417
440
|
|
|
@@ -432,11 +455,17 @@ module Clacky
|
|
|
432
455
|
# Set workspace status to idle (called when agent stops working)
|
|
433
456
|
def set_idle_status
|
|
434
457
|
update_sessionbar(status: 'idle')
|
|
458
|
+
# Clear user tip when agent stops working
|
|
459
|
+
@input_area.clear_user_tip
|
|
460
|
+
@layout.render_input
|
|
435
461
|
end
|
|
436
462
|
|
|
437
463
|
# Set workspace status to working (called when agent starts working)
|
|
438
464
|
def set_working_status
|
|
439
465
|
update_sessionbar(status: 'working')
|
|
466
|
+
# Show a random user tip with 40% probability when agent starts working
|
|
467
|
+
@input_area.show_user_tip(probability: 0.4)
|
|
468
|
+
@layout.render_input
|
|
440
469
|
end
|
|
441
470
|
|
|
442
471
|
# Show help text
|
|
@@ -484,7 +513,7 @@ module Clacky
|
|
|
484
513
|
|
|
485
514
|
# Create InlineInput with styled prompt
|
|
486
515
|
inline_input = Components::InlineInput.new(
|
|
487
|
-
prompt:
|
|
516
|
+
prompt: " Press Enter to approve, 'n' to reject, or provide feedback: ",
|
|
488
517
|
default: nil
|
|
489
518
|
)
|
|
490
519
|
@inline_input = inline_input
|
|
@@ -599,6 +628,35 @@ module Clacky
|
|
|
599
628
|
append_output(content)
|
|
600
629
|
end
|
|
601
630
|
|
|
631
|
+
# Display recent user messages when loading session
|
|
632
|
+
# @param user_messages [Array<String>] Array of recent user message texts
|
|
633
|
+
def display_session_history(user_messages)
|
|
634
|
+
theme = ThemeManager.current_theme
|
|
635
|
+
|
|
636
|
+
# Show logo banner only
|
|
637
|
+
append_output(@welcome_banner.render_logo)
|
|
638
|
+
|
|
639
|
+
# Show simple header
|
|
640
|
+
append_output(theme.format_text("Recent conversation:", :info))
|
|
641
|
+
|
|
642
|
+
# Display each user message with numbering
|
|
643
|
+
user_messages.each_with_index do |msg, index|
|
|
644
|
+
# Truncate long messages
|
|
645
|
+
display_msg = if msg.length > 140
|
|
646
|
+
"#{msg[0..137]}..."
|
|
647
|
+
else
|
|
648
|
+
msg
|
|
649
|
+
end
|
|
650
|
+
|
|
651
|
+
# Show with number and indentation
|
|
652
|
+
append_output(" #{index + 1}. #{display_msg}")
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
# Bottom spacing and continuation prompt
|
|
656
|
+
append_output("")
|
|
657
|
+
append_output(theme.format_text("Session restored. Feel free to continue with your next task.", :success))
|
|
658
|
+
end
|
|
659
|
+
|
|
602
660
|
# Main input loop
|
|
603
661
|
def input_loop
|
|
604
662
|
@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/version.rb
CHANGED