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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +43 -0
- data/docs/ui2-architecture.md +124 -0
- data/lib/clacky/agent.rb +245 -340
- data/lib/clacky/agent_config.rb +1 -7
- data/lib/clacky/cli.rb +156 -397
- data/lib/clacky/client.rb +68 -36
- data/lib/clacky/gitignore_parser.rb +26 -12
- data/lib/clacky/model_pricing.rb +6 -2
- data/lib/clacky/session_manager.rb +6 -2
- data/lib/clacky/tools/glob.rb +65 -9
- data/lib/clacky/tools/grep.rb +4 -120
- data/lib/clacky/tools/run_project.rb +5 -0
- data/lib/clacky/tools/safe_shell.rb +49 -13
- data/lib/clacky/tools/shell.rb +1 -49
- data/lib/clacky/tools/web_fetch.rb +2 -2
- data/lib/clacky/tools/web_search.rb +38 -26
- data/lib/clacky/ui2/README.md +214 -0
- data/lib/clacky/ui2/components/base_component.rb +163 -0
- data/lib/clacky/ui2/components/common_component.rb +89 -0
- data/lib/clacky/ui2/components/inline_input.rb +187 -0
- data/lib/clacky/ui2/components/input_area.rb +1029 -0
- data/lib/clacky/ui2/components/message_component.rb +76 -0
- data/lib/clacky/ui2/components/output_area.rb +112 -0
- data/lib/clacky/ui2/components/todo_area.rb +137 -0
- data/lib/clacky/ui2/components/tool_component.rb +106 -0
- data/lib/clacky/ui2/components/welcome_banner.rb +93 -0
- data/lib/clacky/ui2/layout_manager.rb +331 -0
- data/lib/clacky/ui2/line_editor.rb +201 -0
- data/lib/clacky/ui2/screen_buffer.rb +238 -0
- data/lib/clacky/ui2/theme_manager.rb +68 -0
- data/lib/clacky/ui2/themes/base_theme.rb +99 -0
- data/lib/clacky/ui2/themes/hacker_theme.rb +56 -0
- data/lib/clacky/ui2/themes/minimal_theme.rb +50 -0
- data/lib/clacky/ui2/ui_controller.rb +720 -0
- data/lib/clacky/ui2/view_renderer.rb +160 -0
- data/lib/clacky/ui2.rb +37 -0
- data/lib/clacky/utils/file_ignore_helper.rb +126 -0
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +1 -6
- metadata +38 -6
- data/lib/clacky/ui/banner.rb +0 -155
- data/lib/clacky/ui/enhanced_prompt.rb +0 -786
- data/lib/clacky/ui/formatter.rb +0 -209
- data/lib/clacky/ui/statusbar.rb +0 -96
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_component"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module UI2
|
|
7
|
+
module Components
|
|
8
|
+
# MessageComponent renders user and assistant messages
|
|
9
|
+
class MessageComponent < BaseComponent
|
|
10
|
+
# Render a message
|
|
11
|
+
# @param data [Hash] Message data
|
|
12
|
+
# - :role [String] "user" or "assistant"
|
|
13
|
+
# - :content [String] Message content
|
|
14
|
+
# - :timestamp [Time, nil] Optional timestamp
|
|
15
|
+
# - :images [Array<String>] Optional image paths (for user messages)
|
|
16
|
+
# @return [String] Rendered message
|
|
17
|
+
def render(data)
|
|
18
|
+
role = data[:role]
|
|
19
|
+
content = data[:content]
|
|
20
|
+
timestamp = data[:timestamp]
|
|
21
|
+
images = data[:images] || []
|
|
22
|
+
|
|
23
|
+
case role
|
|
24
|
+
when "user"
|
|
25
|
+
render_user_message(content, timestamp, images)
|
|
26
|
+
when "assistant"
|
|
27
|
+
render_assistant_message(content, timestamp)
|
|
28
|
+
else
|
|
29
|
+
render_system_message(content, timestamp)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
# Render user message
|
|
36
|
+
# @param content [String] Message content
|
|
37
|
+
# @param timestamp [Time, nil] Optional timestamp
|
|
38
|
+
# @param images [Array<String>] Optional image paths
|
|
39
|
+
# @return [String] Rendered message
|
|
40
|
+
def render_user_message(content, timestamp = nil, images = [])
|
|
41
|
+
symbol = format_symbol(:user)
|
|
42
|
+
text = format_text(content, :user)
|
|
43
|
+
time_str = timestamp ? @pastel.dim("[#{format_timestamp(timestamp)}]") : ""
|
|
44
|
+
|
|
45
|
+
"\n#{symbol} #{text} #{time_str}".rstrip
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Render assistant message
|
|
49
|
+
# @param content [String] Message content
|
|
50
|
+
# @param timestamp [Time, nil] Optional timestamp
|
|
51
|
+
# @return [String] Rendered message
|
|
52
|
+
def render_assistant_message(content, timestamp = nil)
|
|
53
|
+
return "" if content.nil? || content.empty?
|
|
54
|
+
|
|
55
|
+
symbol = format_symbol(:assistant)
|
|
56
|
+
text = format_text(content, :assistant)
|
|
57
|
+
time_str = timestamp ? @pastel.dim("[#{format_timestamp(timestamp)}]") : ""
|
|
58
|
+
|
|
59
|
+
"\n#{symbol} #{text} #{time_str}".rstrip
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# Render system message
|
|
63
|
+
# @param content [String] Message content
|
|
64
|
+
# @param timestamp [Time, nil] Optional timestamp
|
|
65
|
+
# @return [String] Rendered message
|
|
66
|
+
def render_system_message(content, timestamp = nil)
|
|
67
|
+
symbol = format_symbol(:info)
|
|
68
|
+
text = format_text(content, :info)
|
|
69
|
+
time_str = timestamp ? @pastel.dim("[#{format_timestamp(timestamp)}]") : ""
|
|
70
|
+
|
|
71
|
+
"\n#{symbol} #{text} #{time_str}".rstrip
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module UI2
|
|
7
|
+
module Components
|
|
8
|
+
# OutputArea writes content directly to terminal
|
|
9
|
+
# Terminal handles scrolling natively via scrollback buffer
|
|
10
|
+
class OutputArea
|
|
11
|
+
attr_accessor :height
|
|
12
|
+
|
|
13
|
+
def initialize(height:)
|
|
14
|
+
@height = height
|
|
15
|
+
@pastel = Pastel.new
|
|
16
|
+
@width = TTY::Screen.width
|
|
17
|
+
@last_line_row = nil # Track last line position for updates
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Append single line content directly to terminal (no newline)
|
|
21
|
+
# Multi-line handling is done by LayoutManager
|
|
22
|
+
# @param content [String] Single line content to append
|
|
23
|
+
def append(content)
|
|
24
|
+
return if content.nil? || content.empty?
|
|
25
|
+
|
|
26
|
+
update_width
|
|
27
|
+
print wrap_line(content)
|
|
28
|
+
flush
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Initial render - no-op, output flows naturally
|
|
32
|
+
# @param start_row [Integer] Screen row (ignored)
|
|
33
|
+
def render(start_row:)
|
|
34
|
+
# No-op - output flows naturally from current position
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Update the last line (for progress indicator)
|
|
38
|
+
# Uses carriage return to overwrite current line
|
|
39
|
+
# @param content [String] New content for last line
|
|
40
|
+
def update_last_line(content)
|
|
41
|
+
print "\r"
|
|
42
|
+
clear_line
|
|
43
|
+
print truncate_line(content)
|
|
44
|
+
flush
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Remove the last line from output
|
|
48
|
+
def remove_last_line
|
|
49
|
+
print "\r"
|
|
50
|
+
clear_line
|
|
51
|
+
flush
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Clear - no-op for natural scroll mode
|
|
55
|
+
def clear
|
|
56
|
+
# No-op
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# Legacy scroll methods (no-op, terminal handles scrolling)
|
|
60
|
+
def scroll_up(lines = 1); end
|
|
61
|
+
def scroll_down(lines = 1); end
|
|
62
|
+
def scroll_to_top; end
|
|
63
|
+
def scroll_to_bottom; end
|
|
64
|
+
def at_bottom?; true; end
|
|
65
|
+
def scroll_percentage; 0.0; end
|
|
66
|
+
|
|
67
|
+
def visible_range
|
|
68
|
+
{ start: 1, end: @height, total: @height }
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Wrap line to fit screen width (auto line wrap)
|
|
74
|
+
def wrap_line(line)
|
|
75
|
+
return "" if line.nil?
|
|
76
|
+
# Let terminal handle line wrapping naturally
|
|
77
|
+
line
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Truncate line to fit screen width
|
|
81
|
+
def truncate_line(line)
|
|
82
|
+
return "" if line.nil?
|
|
83
|
+
|
|
84
|
+
visible_length = line.gsub(/\e\[[0-9;]*m/, "").length
|
|
85
|
+
|
|
86
|
+
if visible_length > @width
|
|
87
|
+
truncated = line[0...(@width - 3)]
|
|
88
|
+
truncated + @pastel.dim("...")
|
|
89
|
+
else
|
|
90
|
+
line
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def update_width
|
|
95
|
+
@width = TTY::Screen.width
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def move_cursor(row, col)
|
|
99
|
+
print "\e[#{row + 1};#{col + 1}H"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def clear_line
|
|
103
|
+
print "\e[2K"
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def flush
|
|
107
|
+
$stdout.flush
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
112
|
+
end
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module UI2
|
|
7
|
+
module Components
|
|
8
|
+
# TodoArea displays active todos above the separator line
|
|
9
|
+
class TodoArea
|
|
10
|
+
attr_accessor :height
|
|
11
|
+
attr_reader :todos
|
|
12
|
+
|
|
13
|
+
MAX_DISPLAY_TASKS = 2 # Show at most 2 tasks (Next + After)
|
|
14
|
+
|
|
15
|
+
def initialize
|
|
16
|
+
@todos = []
|
|
17
|
+
@pastel = Pastel.new
|
|
18
|
+
@width = TTY::Screen.width
|
|
19
|
+
@height = 0 # Dynamic height based on todos
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# Update todos list
|
|
23
|
+
# @param todos [Array<Hash>] Array of todo items
|
|
24
|
+
def update(todos)
|
|
25
|
+
@todos = todos || []
|
|
26
|
+
@pending_todos = @todos.select { |t| t[:status] == "pending" }
|
|
27
|
+
@completed_count = @todos.count { |t| t[:status] == "completed" }
|
|
28
|
+
@total_count = @todos.size
|
|
29
|
+
|
|
30
|
+
# Height: 1 line for header + min(pending_count, MAX_DISPLAY_TASKS) lines for tasks
|
|
31
|
+
if @pending_todos.empty? && @completed_count == 0
|
|
32
|
+
@height = 0
|
|
33
|
+
else
|
|
34
|
+
display_count = [@pending_todos.size, MAX_DISPLAY_TASKS].min
|
|
35
|
+
@height = 1 + display_count
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Check if there are todos to display
|
|
40
|
+
def visible?
|
|
41
|
+
@height > 0
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# Render todos area
|
|
45
|
+
# @param start_row [Integer] Screen row to start rendering
|
|
46
|
+
def render(start_row:)
|
|
47
|
+
return unless visible?
|
|
48
|
+
|
|
49
|
+
update_width
|
|
50
|
+
|
|
51
|
+
# Render header: [##] Tasks [0/4]: ████
|
|
52
|
+
move_cursor(start_row, 0)
|
|
53
|
+
clear_line
|
|
54
|
+
header = render_header
|
|
55
|
+
print header
|
|
56
|
+
|
|
57
|
+
# Render tasks (Next and After)
|
|
58
|
+
@pending_todos.take(MAX_DISPLAY_TASKS).each_with_index do |todo, i|
|
|
59
|
+
move_cursor(start_row + i + 1, 0)
|
|
60
|
+
clear_line
|
|
61
|
+
|
|
62
|
+
label = i == 0 ? "Next" : "After"
|
|
63
|
+
task_text = truncate_text("##{todo[:id]} - #{todo[:task]}", @width - 12)
|
|
64
|
+
line = " #{@pastel.dim("->")} #{@pastel.yellow(label)}: #{task_text}"
|
|
65
|
+
print line
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
flush
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
# Clear the area
|
|
72
|
+
def clear
|
|
73
|
+
@todos = []
|
|
74
|
+
@pending_todos = []
|
|
75
|
+
@completed_count = 0
|
|
76
|
+
@total_count = 0
|
|
77
|
+
@height = 0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
private
|
|
81
|
+
|
|
82
|
+
# Render header line with progress bar
|
|
83
|
+
def render_header
|
|
84
|
+
progress = "#{@completed_count}/#{@total_count}"
|
|
85
|
+
progress_bar = render_progress_bar(@completed_count, @total_count)
|
|
86
|
+
|
|
87
|
+
"#{@pastel.cyan("[##]")} Tasks [#{progress}]: #{progress_bar}"
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Render a simple progress bar
|
|
91
|
+
def render_progress_bar(completed, total)
|
|
92
|
+
return "" if total == 0
|
|
93
|
+
|
|
94
|
+
bar_width = 10
|
|
95
|
+
filled = total > 0 ? (completed.to_f / total * bar_width).round : 0
|
|
96
|
+
empty = bar_width - filled
|
|
97
|
+
|
|
98
|
+
filled_bar = @pastel.green("█" * filled)
|
|
99
|
+
empty_bar = @pastel.dim("░" * empty)
|
|
100
|
+
|
|
101
|
+
"#{filled_bar}#{empty_bar}"
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Truncate text to fit width
|
|
105
|
+
def truncate_text(text, max_width)
|
|
106
|
+
return "" if text.nil?
|
|
107
|
+
|
|
108
|
+
if text.length > max_width
|
|
109
|
+
text[0...(max_width - 3)] + "..."
|
|
110
|
+
else
|
|
111
|
+
text
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Update width on resize
|
|
116
|
+
def update_width
|
|
117
|
+
@width = TTY::Screen.width
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Move cursor to position
|
|
121
|
+
def move_cursor(row, col)
|
|
122
|
+
print "\e[#{row + 1};#{col + 1}H"
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Clear current line
|
|
126
|
+
def clear_line
|
|
127
|
+
print "\e[2K"
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
# Flush output
|
|
131
|
+
def flush
|
|
132
|
+
$stdout.flush
|
|
133
|
+
end
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
end
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_component"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module UI2
|
|
7
|
+
module Components
|
|
8
|
+
# ToolComponent renders tool calls and results
|
|
9
|
+
class ToolComponent < BaseComponent
|
|
10
|
+
# Render a tool event
|
|
11
|
+
# @param data [Hash] Tool event data
|
|
12
|
+
# - :type [Symbol] :call, :result, :error, :denied, :planned
|
|
13
|
+
# - :tool_name [String] Name of the tool
|
|
14
|
+
# - :formatted_call [String] Formatted tool call description
|
|
15
|
+
# - :result [String] Tool result (for :result type)
|
|
16
|
+
# - :error [String] Error message (for :error type)
|
|
17
|
+
# @return [String] Rendered tool event
|
|
18
|
+
def render(data)
|
|
19
|
+
type = data[:type]
|
|
20
|
+
|
|
21
|
+
case type
|
|
22
|
+
when :call
|
|
23
|
+
render_tool_call(data)
|
|
24
|
+
when :result
|
|
25
|
+
render_tool_result(data)
|
|
26
|
+
when :error
|
|
27
|
+
render_tool_error(data)
|
|
28
|
+
when :denied
|
|
29
|
+
render_tool_denied(data)
|
|
30
|
+
when :planned
|
|
31
|
+
render_tool_planned(data)
|
|
32
|
+
else
|
|
33
|
+
render_unknown_tool_event(data)
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# Render tool call
|
|
40
|
+
# @param data [Hash] Tool call data
|
|
41
|
+
# @return [String] Rendered tool call
|
|
42
|
+
def render_tool_call(data)
|
|
43
|
+
symbol = format_symbol(:tool_call)
|
|
44
|
+
formatted_call = data[:formatted_call] || "#{data[:tool_name]}(...)"
|
|
45
|
+
text = format_text(formatted_call, :tool_call)
|
|
46
|
+
|
|
47
|
+
"\n#{symbol} #{text}"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
# Render tool result
|
|
51
|
+
# @param data [Hash] Tool result data
|
|
52
|
+
# @return [String] Rendered tool result
|
|
53
|
+
def render_tool_result(data)
|
|
54
|
+
symbol = format_symbol(:tool_result)
|
|
55
|
+
result = data[:result] || data[:summary] || "completed"
|
|
56
|
+
text = format_text(truncate(result, 200), :tool_result)
|
|
57
|
+
|
|
58
|
+
"#{symbol} #{text}"
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Render tool error
|
|
62
|
+
# @param data [Hash] Tool error data
|
|
63
|
+
# @return [String] Rendered tool error
|
|
64
|
+
def render_tool_error(data)
|
|
65
|
+
symbol = format_symbol(:tool_error)
|
|
66
|
+
error_msg = data[:error] || "Unknown error"
|
|
67
|
+
text = format_text("Error: #{error_msg}", :tool_error)
|
|
68
|
+
|
|
69
|
+
"\n#{symbol} #{text}"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Render tool denied
|
|
73
|
+
# @param data [Hash] Tool denied data
|
|
74
|
+
# @return [String] Rendered tool denied
|
|
75
|
+
def render_tool_denied(data)
|
|
76
|
+
symbol = format_symbol(:tool_denied)
|
|
77
|
+
tool_name = data[:tool_name] || "unknown"
|
|
78
|
+
text = format_text("Tool denied: #{tool_name}", :tool_denied)
|
|
79
|
+
|
|
80
|
+
"\n#{symbol} #{text}"
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
# Render tool planned
|
|
84
|
+
# @param data [Hash] Tool planned data
|
|
85
|
+
# @return [String] Rendered tool planned
|
|
86
|
+
def render_tool_planned(data)
|
|
87
|
+
symbol = format_symbol(:tool_planned)
|
|
88
|
+
tool_name = data[:tool_name] || "unknown"
|
|
89
|
+
text = format_text("Planned: #{tool_name}", :tool_planned)
|
|
90
|
+
|
|
91
|
+
"\n#{symbol} #{text}"
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
# Render unknown tool event
|
|
95
|
+
# @param data [Hash] Tool event data
|
|
96
|
+
# @return [String] Rendered unknown event
|
|
97
|
+
def render_unknown_tool_event(data)
|
|
98
|
+
symbol = format_symbol(:info)
|
|
99
|
+
text = format_text("Tool event: #{data.inspect}", :info)
|
|
100
|
+
|
|
101
|
+
"#{symbol} #{text}"
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module UI2
|
|
7
|
+
module Components
|
|
8
|
+
# WelcomeBanner displays the startup screen with ASCII logo, tagline, tips, and agent info
|
|
9
|
+
class WelcomeBanner
|
|
10
|
+
LOGO = <<~'LOGO'
|
|
11
|
+
██████╗ ██████╗ ███████╗███╗ ██╗ ██████╗██╗ █████╗ ██████╗██╗ ██╗██╗ ██╗
|
|
12
|
+
██╔═══██╗██╔══██╗██╔════╝████╗ ██║██╔════╝██║ ██╔══██╗██╔════╝██║ ██╔╝╚██╗ ██╔╝
|
|
13
|
+
██║ ██║██████╔╝█████╗ ██╔██╗ ██║██║ ██║ ███████║██║ █████╔╝ ╚████╔╝
|
|
14
|
+
██║ ██║██╔═══╝ ██╔══╝ ██║╚██╗██║██║ ██║ ██╔══██║██║ ██╔═██╗ ╚██╔╝
|
|
15
|
+
╚██████╔╝██║ ███████╗██║ ╚████║╚██████╗███████╗██║ ██║╚██████╗██║ ██╗ ██║
|
|
16
|
+
╚═════╝ ╚═╝ ╚══════╝╚═╝ ╚═══╝ ╚═════╝╚══════╝╚═╝ ╚═╝ ╚═════╝╚═╝ ╚═╝ ╚═╝
|
|
17
|
+
LOGO
|
|
18
|
+
|
|
19
|
+
TAGLINE = "[>] AI Coding Assistant & Technical Co-founder"
|
|
20
|
+
|
|
21
|
+
TIPS = [
|
|
22
|
+
"[*] Ask questions, edit files, or run commands",
|
|
23
|
+
"[*] Be specific for the best results",
|
|
24
|
+
"[*] Create .clackyrules to customize interactions",
|
|
25
|
+
"[*] Type /help for more commands"
|
|
26
|
+
].freeze
|
|
27
|
+
|
|
28
|
+
def initialize
|
|
29
|
+
@pastel = Pastel.new
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# Render startup banner
|
|
33
|
+
# @return [String] Formatted startup banner
|
|
34
|
+
def render_startup
|
|
35
|
+
lines = []
|
|
36
|
+
lines << ""
|
|
37
|
+
lines << @pastel.bright_green(LOGO)
|
|
38
|
+
lines << ""
|
|
39
|
+
lines << @pastel.bright_cyan(TAGLINE)
|
|
40
|
+
lines << ""
|
|
41
|
+
TIPS.each do |tip|
|
|
42
|
+
lines << @pastel.dim(tip)
|
|
43
|
+
end
|
|
44
|
+
lines << ""
|
|
45
|
+
lines.join("\n")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# Render agent welcome section
|
|
49
|
+
# @param working_dir [String] Working directory
|
|
50
|
+
# @param mode [String] Permission mode
|
|
51
|
+
# @return [String] Formatted agent welcome section
|
|
52
|
+
def render_agent_welcome(working_dir:, mode:)
|
|
53
|
+
lines = []
|
|
54
|
+
lines << ""
|
|
55
|
+
lines << separator("=")
|
|
56
|
+
lines << @pastel.bright_green("[+] AGENT MODE INITIALIZED")
|
|
57
|
+
lines << separator("=")
|
|
58
|
+
lines << ""
|
|
59
|
+
lines << info_line("Working Directory", working_dir)
|
|
60
|
+
lines << info_line("Permission Mode", mode)
|
|
61
|
+
lines << ""
|
|
62
|
+
lines << @pastel.dim("[!] Type 'exit' or 'quit' to terminate session")
|
|
63
|
+
lines << separator("-")
|
|
64
|
+
lines << ""
|
|
65
|
+
lines.join("\n")
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Render full welcome (startup + agent info)
|
|
69
|
+
# @param working_dir [String] Working directory
|
|
70
|
+
# @param mode [String] Permission mode
|
|
71
|
+
# @return [String] Full welcome content
|
|
72
|
+
def render_full(working_dir:, mode:)
|
|
73
|
+
render_startup + render_agent_welcome(
|
|
74
|
+
working_dir: working_dir,
|
|
75
|
+
mode: mode
|
|
76
|
+
)
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
private
|
|
80
|
+
|
|
81
|
+
def info_line(label, value)
|
|
82
|
+
label_text = @pastel.cyan("[#{label}]")
|
|
83
|
+
value_text = @pastel.white(value)
|
|
84
|
+
" #{label_text} #{value_text}"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def separator(char = "-")
|
|
88
|
+
@pastel.dim(char * 80)
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|