openclacky 0.5.6 → 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 +71 -0
- data/docs/ui2-architecture.md +124 -0
- data/lib/clacky/agent.rb +376 -346
- data/lib/clacky/agent_config.rb +1 -7
- data/lib/clacky/cli.rb +167 -398
- 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 +66 -10
- data/lib/clacky/tools/grep.rb +6 -122
- data/lib/clacky/tools/run_project.rb +10 -5
- data/lib/clacky/tools/safe_shell.rb +149 -20
- data/lib/clacky/tools/shell.rb +3 -51
- 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 +4 -4
- data/lib/clacky/tools/web_search.rb +40 -28
- 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 +98 -0
- data/lib/clacky/ui2/components/inline_input.rb +187 -0
- data/lib/clacky/ui2/components/input_area.rb +1124 -0
- data/lib/clacky/ui2/components/message_component.rb +80 -0
- data/lib/clacky/ui2/components/output_area.rb +112 -0
- data/lib/clacky/ui2/components/todo_area.rb +130 -0
- data/lib/clacky/ui2/components/tool_component.rb +106 -0
- data/lib/clacky/ui2/components/welcome_banner.rb +103 -0
- data/lib/clacky/ui2/layout_manager.rb +437 -0
- data/lib/clacky/ui2/line_editor.rb +201 -0
- data/lib/clacky/ui2/markdown_renderer.rb +80 -0
- data/lib/clacky/ui2/screen_buffer.rb +257 -0
- data/lib/clacky/ui2/theme_manager.rb +68 -0
- data/lib/clacky/ui2/themes/base_theme.rb +85 -0
- data/lib/clacky/ui2/themes/hacker_theme.rb +58 -0
- data/lib/clacky/ui2/themes/minimal_theme.rb +52 -0
- data/lib/clacky/ui2/ui_controller.rb +778 -0
- data/lib/clacky/ui2/view_renderer.rb +177 -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 +53 -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,214 @@
|
|
|
1
|
+
# Clacky UI2 - MVC Terminal UI System
|
|
2
|
+
|
|
3
|
+
A modern, MVC-based terminal UI system with split-screen layout, component-based rendering, and direct method calls for simplicity.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **Split-Screen Layout**: Scrollable output area on top, fixed input area at bottom
|
|
8
|
+
- **MVC Architecture**: Clean separation of concerns (Model-View-Controller)
|
|
9
|
+
- **Direct Method Calls**: Agent directly calls UIController semantic methods
|
|
10
|
+
- **Component-Based**: Reusable, composable UI components
|
|
11
|
+
- **Scrollable Output**: Navigate through history with arrow keys
|
|
12
|
+
- **Input History**: Navigate previous inputs with up/down arrows
|
|
13
|
+
- **Responsive**: Handles terminal resize automatically
|
|
14
|
+
- **Rich Formatting**: Colored output with Pastel integration
|
|
15
|
+
|
|
16
|
+
## Architecture
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
+---------------------------------------------+
|
|
20
|
+
| Business Layer (Agent) |
|
|
21
|
+
| Agent directly calls @ui.show_xxx() |
|
|
22
|
+
+-----------------------+---------------------+
|
|
23
|
+
| Direct calls
|
|
24
|
+
v
|
|
25
|
+
+---------------------------------------------+
|
|
26
|
+
| Controller Layer (UIController) |
|
|
27
|
+
| - show_assistant_message() |
|
|
28
|
+
| - show_tool_call() |
|
|
29
|
+
| - show_tool_result() |
|
|
30
|
+
| - request_confirmation() |
|
|
31
|
+
+-----------------------+---------------------+
|
|
32
|
+
| Render
|
|
33
|
+
v
|
|
34
|
+
+---------------------------------------------+
|
|
35
|
+
| View Layer (ViewRenderer) |
|
|
36
|
+
| - Components (Message, Tool, Status) |
|
|
37
|
+
+---------------------------------------------+
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Quick Start
|
|
41
|
+
|
|
42
|
+
### Agent Integration
|
|
43
|
+
|
|
44
|
+
```ruby
|
|
45
|
+
# Create UI controller
|
|
46
|
+
ui_controller = Clacky::UI2::UIController.new(
|
|
47
|
+
working_dir: Dir.pwd,
|
|
48
|
+
mode: "confirm_safes",
|
|
49
|
+
model: "claude-3-5-sonnet"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Create agent with UI injected
|
|
53
|
+
agent = Clacky::Agent.new(client, config, ui: ui_controller)
|
|
54
|
+
|
|
55
|
+
# Set up input handler
|
|
56
|
+
ui_controller.on_input do |input, images|
|
|
57
|
+
result = agent.run(input, images: images)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Start UI (blocks until exit)
|
|
61
|
+
ui_controller.start
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
### UIController Semantic Methods
|
|
65
|
+
|
|
66
|
+
The Agent calls these methods directly:
|
|
67
|
+
|
|
68
|
+
```ruby
|
|
69
|
+
# Show messages
|
|
70
|
+
@ui.show_assistant_message("Hello!")
|
|
71
|
+
@ui.show_user_message("Hi there", images: [])
|
|
72
|
+
|
|
73
|
+
# Show tool operations
|
|
74
|
+
@ui.show_tool_call("file_reader", { path: "test.rb" })
|
|
75
|
+
@ui.show_tool_result("File contents...")
|
|
76
|
+
@ui.show_tool_error("File not found")
|
|
77
|
+
|
|
78
|
+
# Show status
|
|
79
|
+
@ui.show_progress("Thinking...")
|
|
80
|
+
@ui.clear_progress
|
|
81
|
+
@ui.show_complete(iterations: 5, cost: 0.001)
|
|
82
|
+
|
|
83
|
+
# Show info/warning/error
|
|
84
|
+
@ui.show_info("Session saved")
|
|
85
|
+
@ui.show_warning("Rate limited")
|
|
86
|
+
@ui.show_error("Failed to connect")
|
|
87
|
+
|
|
88
|
+
# Interactive confirmation
|
|
89
|
+
result = @ui.request_confirmation("Allow file write?", default: true)
|
|
90
|
+
# Returns: true/false for yes/no, String for feedback, nil for cancelled
|
|
91
|
+
|
|
92
|
+
# Show diff
|
|
93
|
+
@ui.show_diff(old_content, new_content, max_lines: 50)
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
## Components
|
|
97
|
+
|
|
98
|
+
### ViewRenderer
|
|
99
|
+
|
|
100
|
+
Unified interface for rendering all UI components.
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
renderer = Clacky::UI2::ViewRenderer.new
|
|
104
|
+
|
|
105
|
+
# Render messages
|
|
106
|
+
renderer.render_user_message("Hello")
|
|
107
|
+
renderer.render_assistant_message("Hi there")
|
|
108
|
+
|
|
109
|
+
# Render tools
|
|
110
|
+
renderer.render_tool_call(
|
|
111
|
+
tool_name: "file_reader",
|
|
112
|
+
formatted_call: "file_reader(path: 'test.rb')"
|
|
113
|
+
)
|
|
114
|
+
renderer.render_tool_result(result: "Success")
|
|
115
|
+
|
|
116
|
+
# Render status
|
|
117
|
+
renderer.render_status(
|
|
118
|
+
iteration: 5,
|
|
119
|
+
cost: 0.1234,
|
|
120
|
+
tasks_completed: 3,
|
|
121
|
+
tasks_total: 10
|
|
122
|
+
)
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
### OutputArea
|
|
126
|
+
|
|
127
|
+
Scrollable output buffer with automatic line wrapping.
|
|
128
|
+
|
|
129
|
+
```ruby
|
|
130
|
+
output = Clacky::UI2::Components::OutputArea.new(height: 20)
|
|
131
|
+
|
|
132
|
+
output.append("Line 1")
|
|
133
|
+
output.append("Line 2\nLine 3")
|
|
134
|
+
|
|
135
|
+
output.scroll_up(5)
|
|
136
|
+
output.scroll_down(2)
|
|
137
|
+
output.scroll_to_top
|
|
138
|
+
output.scroll_to_bottom
|
|
139
|
+
|
|
140
|
+
output.at_bottom? # => true/false
|
|
141
|
+
output.scroll_percentage # => 0.0 to 100.0
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
### InputArea
|
|
145
|
+
|
|
146
|
+
Fixed input area with cursor support and history.
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
input = Clacky::UI2::Components::InputArea.new(height: 2)
|
|
150
|
+
|
|
151
|
+
input.insert_char("H")
|
|
152
|
+
input.backspace
|
|
153
|
+
input.cursor_left
|
|
154
|
+
input.cursor_right
|
|
155
|
+
|
|
156
|
+
value = input.submit # Returns and clears input
|
|
157
|
+
input.history_prev # Navigate history
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
### LayoutManager
|
|
161
|
+
|
|
162
|
+
Manages screen layout and coordinates rendering.
|
|
163
|
+
|
|
164
|
+
```ruby
|
|
165
|
+
layout = Clacky::UI2::LayoutManager.new(
|
|
166
|
+
output_area: output,
|
|
167
|
+
input_area: input
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
layout.initialize_screen
|
|
171
|
+
layout.append_output("Hello")
|
|
172
|
+
layout.move_input_to_output
|
|
173
|
+
layout.scroll_output_up(5)
|
|
174
|
+
layout.cleanup_screen
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## Keyboard Shortcuts
|
|
178
|
+
|
|
179
|
+
- **Enter** - Submit input
|
|
180
|
+
- **Ctrl+C** - Exit/Interrupt
|
|
181
|
+
- **Ctrl+L** - Clear output
|
|
182
|
+
- **Ctrl+U** - Clear input line
|
|
183
|
+
- **Up/Down** - Scroll output (when input empty) or navigate history
|
|
184
|
+
- **Left/Right** - Move cursor in input
|
|
185
|
+
- **Home/End** - Jump to start/end of input
|
|
186
|
+
- **Backspace** - Delete character before cursor
|
|
187
|
+
- **Delete** - Delete character at cursor
|
|
188
|
+
|
|
189
|
+
## Layout Structure
|
|
190
|
+
|
|
191
|
+
```
|
|
192
|
+
+----------------------------------------+
|
|
193
|
+
| Output Area (Scrollable) | <- Lines 0 to height-4
|
|
194
|
+
| [<<] Assistant: Hello... |
|
|
195
|
+
| [=>] Tool: file_reader |
|
|
196
|
+
| [<=] Result: ... |
|
|
197
|
+
| ... |
|
|
198
|
+
+----------------------------------------+ <- Separator
|
|
199
|
+
| [>>] Input: _ | <- Input line
|
|
200
|
+
+----------------------------------------+ <- Session bar
|
|
201
|
+
| Mode: confirm_safes | Tasks: 5 | $0.01 |
|
|
202
|
+
+----------------------------------------+
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
## Design Principles
|
|
206
|
+
|
|
207
|
+
1. **Simplicity**: Agent directly calls UIController methods - no middleware
|
|
208
|
+
2. **Dependency Injection**: Agent receives `ui:` parameter
|
|
209
|
+
3. **Component-Based**: Reusable, testable UI components
|
|
210
|
+
4. **Responsive**: Handles terminal resize and edge cases
|
|
211
|
+
|
|
212
|
+
## License
|
|
213
|
+
|
|
214
|
+
MIT
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pastel"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module UI2
|
|
7
|
+
module Components
|
|
8
|
+
# BaseComponent provides common functionality for all UI components
|
|
9
|
+
class BaseComponent
|
|
10
|
+
def initialize
|
|
11
|
+
@pastel = Pastel.new
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Render component with given data
|
|
15
|
+
# @param data [Hash] Data to render
|
|
16
|
+
# @return [String] Rendered output
|
|
17
|
+
def render(data)
|
|
18
|
+
raise NotImplementedError, "Subclasses must implement render method"
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Class method to render without instantiating
|
|
22
|
+
# @param data [Hash] Data to render
|
|
23
|
+
# @return [String] Rendered output
|
|
24
|
+
def self.render(data)
|
|
25
|
+
new.render(data)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
protected
|
|
29
|
+
|
|
30
|
+
# Get current theme from ThemeManager
|
|
31
|
+
# @return [Themes::BaseTheme] Current theme instance
|
|
32
|
+
def theme
|
|
33
|
+
UI2::ThemeManager.current_theme
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Format symbol with color from theme
|
|
37
|
+
# @param symbol_key [Symbol] Symbol key (e.g., :user, :assistant)
|
|
38
|
+
# @return [String] Colored symbol
|
|
39
|
+
def format_symbol(symbol_key)
|
|
40
|
+
theme.format_symbol(symbol_key)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Format text with color from theme
|
|
44
|
+
# @param text [String] Text to format
|
|
45
|
+
# @param symbol_key [Symbol] Symbol key for color lookup
|
|
46
|
+
# @return [String] Colored text
|
|
47
|
+
def format_text(text, symbol_key)
|
|
48
|
+
theme.format_text(text, symbol_key)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Truncate text to max length
|
|
52
|
+
# @param text [String] Text to truncate
|
|
53
|
+
# @param max_length [Integer] Maximum length
|
|
54
|
+
# @return [String] Truncated text
|
|
55
|
+
def truncate(text, max_length)
|
|
56
|
+
return "" if text.nil? || text.empty?
|
|
57
|
+
|
|
58
|
+
cleaned = text.strip.gsub(/\s+/, ' ')
|
|
59
|
+
|
|
60
|
+
if cleaned.length > max_length
|
|
61
|
+
cleaned[0...max_length] + "..."
|
|
62
|
+
else
|
|
63
|
+
cleaned
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Wrap text to specified width
|
|
68
|
+
# @param text [String] Text to wrap
|
|
69
|
+
# @param width [Integer] Maximum width
|
|
70
|
+
# @return [Array<String>] Array of wrapped lines
|
|
71
|
+
def wrap_text(text, width)
|
|
72
|
+
return [] if text.nil? || text.empty?
|
|
73
|
+
|
|
74
|
+
words = text.split(/\s+/)
|
|
75
|
+
lines = []
|
|
76
|
+
current_line = ""
|
|
77
|
+
|
|
78
|
+
words.each do |word|
|
|
79
|
+
if current_line.empty?
|
|
80
|
+
current_line = word
|
|
81
|
+
elsif (current_line.length + word.length + 1) <= width
|
|
82
|
+
current_line += " #{word}"
|
|
83
|
+
else
|
|
84
|
+
lines << current_line
|
|
85
|
+
current_line = word
|
|
86
|
+
end
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
lines << current_line unless current_line.empty?
|
|
90
|
+
lines
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Format timestamp
|
|
94
|
+
# @param time [Time] Time object
|
|
95
|
+
# @return [String] Formatted timestamp
|
|
96
|
+
def format_timestamp(time = Time.now)
|
|
97
|
+
time.strftime("%H:%M:%S")
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Create indented text
|
|
101
|
+
# @param text [String] Text to indent
|
|
102
|
+
# @param spaces [Integer] Number of spaces
|
|
103
|
+
# @return [String] Indented text
|
|
104
|
+
def indent(text, spaces = 2)
|
|
105
|
+
prefix = " " * spaces
|
|
106
|
+
text.split("\n").map { |line| "#{prefix}#{line}" }.join("\n")
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Format key-value pair
|
|
110
|
+
# @param key [String] Key name
|
|
111
|
+
# @param value [String] Value
|
|
112
|
+
# @return [String] Formatted key-value
|
|
113
|
+
def format_key_value(key, value)
|
|
114
|
+
"#{@pastel.cyan(key)}: #{@pastel.white(value)}"
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
# Create a separator line
|
|
118
|
+
# @param char [String] Character to use
|
|
119
|
+
# @param width [Integer] Width of separator
|
|
120
|
+
# @return [String] Separator line
|
|
121
|
+
def separator(char = "─", width = 80)
|
|
122
|
+
@pastel.dim(char * width)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
# Format list item
|
|
126
|
+
# @param text [String] Item text
|
|
127
|
+
# @param bullet [String] Bullet character
|
|
128
|
+
# @return [String] Formatted list item
|
|
129
|
+
def format_list_item(text, bullet = "•")
|
|
130
|
+
"#{@pastel.dim(bullet)} #{@pastel.white(text)}"
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Format code block
|
|
134
|
+
# @param code [String] Code content
|
|
135
|
+
# @param language [String, nil] Language for syntax highlighting hint
|
|
136
|
+
# @return [String] Formatted code block
|
|
137
|
+
def format_code_block(code, language = nil)
|
|
138
|
+
header = language ? @pastel.dim("```#{language}") : @pastel.dim("```")
|
|
139
|
+
footer = @pastel.dim("```")
|
|
140
|
+
content = @pastel.cyan(code)
|
|
141
|
+
|
|
142
|
+
"#{header}\n#{content}\n#{footer}"
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
# Format progress bar
|
|
146
|
+
# @param current [Integer] Current value
|
|
147
|
+
# @param total [Integer] Total value
|
|
148
|
+
# @param width [Integer] Bar width
|
|
149
|
+
# @return [String] Progress bar
|
|
150
|
+
def format_progress_bar(current, total, width = 20)
|
|
151
|
+
return "" if total == 0
|
|
152
|
+
|
|
153
|
+
percentage = (current.to_f / total * 100).round(1)
|
|
154
|
+
filled = (current.to_f / total * width).round
|
|
155
|
+
empty = width - filled
|
|
156
|
+
|
|
157
|
+
bar = @pastel.green("█" * filled) + @pastel.dim("░" * empty)
|
|
158
|
+
"#{bar} #{percentage}%"
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "base_component"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module UI2
|
|
7
|
+
module Components
|
|
8
|
+
# CommonComponent renders common UI elements (progress, success, error, warning)
|
|
9
|
+
class CommonComponent < BaseComponent
|
|
10
|
+
# Render thinking indicator
|
|
11
|
+
# @return [String] Thinking indicator
|
|
12
|
+
def render_thinking
|
|
13
|
+
symbol = format_symbol(:thinking)
|
|
14
|
+
text = format_text("Thinking...", :thinking)
|
|
15
|
+
"#{symbol} #{text}"
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Render progress indicator (stopped state, gray)
|
|
19
|
+
# @param message [String] Progress message
|
|
20
|
+
# @return [String] Progress indicator
|
|
21
|
+
def render_progress(message)
|
|
22
|
+
symbol = format_symbol(:thinking)
|
|
23
|
+
text = format_text(message, :thinking)
|
|
24
|
+
"#{symbol} #{text}"
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# Render working indicator (active state, yellow)
|
|
28
|
+
# @param message [String] Progress message
|
|
29
|
+
# @return [String] Working indicator
|
|
30
|
+
def render_working(message)
|
|
31
|
+
symbol = format_symbol(:working)
|
|
32
|
+
text = format_text(message, :working)
|
|
33
|
+
"#{symbol} #{text}"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Render success message
|
|
37
|
+
# @param message [String] Success message
|
|
38
|
+
# @return [String] Success message
|
|
39
|
+
def render_success(message)
|
|
40
|
+
symbol = format_symbol(:success)
|
|
41
|
+
text = format_text(message, :success)
|
|
42
|
+
"#{symbol} #{text}"
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Render error message
|
|
46
|
+
# @param message [String] Error message
|
|
47
|
+
# @return [String] Error message
|
|
48
|
+
def render_error(message)
|
|
49
|
+
symbol = format_symbol(:error)
|
|
50
|
+
text = format_text(message, :error)
|
|
51
|
+
"#{symbol} #{text}"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
# Render warning message
|
|
55
|
+
# @param message [String] Warning message
|
|
56
|
+
# @return [String] Warning message
|
|
57
|
+
def render_warning(message)
|
|
58
|
+
symbol = format_symbol(:warning)
|
|
59
|
+
text = format_text(message, :warning)
|
|
60
|
+
"#{symbol} #{text}"
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# Render task completion summary
|
|
64
|
+
# @param iterations [Integer] Number of iterations
|
|
65
|
+
# @param cost [Float] Cost in USD
|
|
66
|
+
# @param duration [Float] Duration in seconds
|
|
67
|
+
# @param cache_tokens [Integer] Cache read tokens
|
|
68
|
+
# @param cache_requests [Integer] Total cache requests count
|
|
69
|
+
# @param cache_hits [Integer] Cache hit requests count
|
|
70
|
+
# @return [String] Formatted completion summary
|
|
71
|
+
def render_task_complete(iterations:, cost:, duration: nil, cache_tokens: nil, cache_requests: nil, cache_hits: nil)
|
|
72
|
+
lines = []
|
|
73
|
+
lines << ""
|
|
74
|
+
lines << @pastel.dim("─" * 60)
|
|
75
|
+
lines << render_success("Task Complete")
|
|
76
|
+
lines << ""
|
|
77
|
+
|
|
78
|
+
# Display each stat on a separate line
|
|
79
|
+
lines << " Iterations: #{iterations}"
|
|
80
|
+
lines << " Cost: $#{cost.round(4)}"
|
|
81
|
+
lines << " Duration: #{duration.round(1)}s" if duration
|
|
82
|
+
|
|
83
|
+
# Display cache information if available
|
|
84
|
+
if cache_tokens && cache_tokens > 0
|
|
85
|
+
lines << " Cache Tokens: #{cache_tokens} tokens"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
if cache_requests && cache_requests > 0
|
|
89
|
+
hit_rate = cache_hits > 0 ? ((cache_hits.to_f / cache_requests) * 100).round(1) : 0
|
|
90
|
+
lines << " Cache Requests: #{cache_requests} (#{cache_hits} hits, #{hit_rate}% hit rate)"
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
lines.join("\n")
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../line_editor"
|
|
4
|
+
|
|
5
|
+
module Clacky
|
|
6
|
+
module UI2
|
|
7
|
+
module Components
|
|
8
|
+
# InlineInput provides inline input for confirmations and simple prompts
|
|
9
|
+
# Renders at the end of output area, not at fixed bottom position
|
|
10
|
+
class InlineInput
|
|
11
|
+
include LineEditor
|
|
12
|
+
|
|
13
|
+
attr_reader :prompt, :default_value
|
|
14
|
+
|
|
15
|
+
def initialize(prompt: "", default: nil)
|
|
16
|
+
initialize_line_editor
|
|
17
|
+
@prompt = prompt
|
|
18
|
+
@default_value = default
|
|
19
|
+
@active = false
|
|
20
|
+
@result_queue = nil
|
|
21
|
+
@paste_counter = 0
|
|
22
|
+
@paste_placeholders = {}
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Activate inline input and wait for user input
|
|
26
|
+
# @return [String] User input
|
|
27
|
+
def collect
|
|
28
|
+
@active = true
|
|
29
|
+
@result_queue = Queue.new
|
|
30
|
+
# Don't set default as initial text - start empty
|
|
31
|
+
@result_queue.pop
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Check if active
|
|
35
|
+
def active?
|
|
36
|
+
@active
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Handle keyboard input
|
|
40
|
+
# @param key [Symbol, String] Key input
|
|
41
|
+
# @return [Hash] Result with action
|
|
42
|
+
def handle_key(key)
|
|
43
|
+
return { action: nil } unless @active
|
|
44
|
+
|
|
45
|
+
case key
|
|
46
|
+
when Hash
|
|
47
|
+
if key[:type] == :rapid_input
|
|
48
|
+
# Handle multi-line paste with placeholder
|
|
49
|
+
pasted_text = key[:text]
|
|
50
|
+
pasted_lines = pasted_text.split(/\r\n|\r|\n/)
|
|
51
|
+
|
|
52
|
+
if pasted_lines.size > 1
|
|
53
|
+
# Multi-line paste - use placeholder
|
|
54
|
+
@paste_counter += 1
|
|
55
|
+
placeholder = "[##{@paste_counter} Paste Text]"
|
|
56
|
+
@paste_placeholders[placeholder] = pasted_text
|
|
57
|
+
insert_text(placeholder)
|
|
58
|
+
else
|
|
59
|
+
# Single line - insert directly
|
|
60
|
+
insert_text(pasted_text)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
{ action: :update }
|
|
64
|
+
when :enter
|
|
65
|
+
handle_enter
|
|
66
|
+
when :backspace
|
|
67
|
+
backspace
|
|
68
|
+
{ action: :update }
|
|
69
|
+
when :delete
|
|
70
|
+
delete_char
|
|
71
|
+
{ action: :update }
|
|
72
|
+
when :left_arrow, :ctrl_b
|
|
73
|
+
cursor_left
|
|
74
|
+
{ action: :update }
|
|
75
|
+
when :right_arrow, :ctrl_f
|
|
76
|
+
cursor_right
|
|
77
|
+
{ action: :update }
|
|
78
|
+
when :home, :ctrl_a
|
|
79
|
+
cursor_home
|
|
80
|
+
{ action: :update }
|
|
81
|
+
when :end, :ctrl_e
|
|
82
|
+
cursor_end
|
|
83
|
+
{ action: :update }
|
|
84
|
+
when :ctrl_k
|
|
85
|
+
kill_to_end
|
|
86
|
+
{ action: :update }
|
|
87
|
+
when :ctrl_u
|
|
88
|
+
kill_to_start
|
|
89
|
+
{ action: :update }
|
|
90
|
+
when :ctrl_w
|
|
91
|
+
kill_word
|
|
92
|
+
{ action: :update }
|
|
93
|
+
when :shift_tab
|
|
94
|
+
handle_shift_tab
|
|
95
|
+
when :ctrl_c
|
|
96
|
+
handle_cancel
|
|
97
|
+
when :escape
|
|
98
|
+
handle_cancel
|
|
99
|
+
else
|
|
100
|
+
if key.is_a?(String) && key.length >= 1 && key.ord >= 32
|
|
101
|
+
insert_char(key)
|
|
102
|
+
{ action: :update }
|
|
103
|
+
else
|
|
104
|
+
{ action: nil }
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
# Render inline input with prompt and cursor
|
|
110
|
+
# @return [String] Rendered line (may wrap to multiple lines)
|
|
111
|
+
def render
|
|
112
|
+
line = render_line_with_cursor
|
|
113
|
+
full_text = "#{@prompt}#{line}"
|
|
114
|
+
|
|
115
|
+
# Calculate terminal width and check if wrapping is needed
|
|
116
|
+
width = TTY::Screen.width
|
|
117
|
+
visible_text = strip_ansi_codes(full_text)
|
|
118
|
+
display_width = calculate_display_width(visible_text)
|
|
119
|
+
|
|
120
|
+
# If no wrapping needed, return as is
|
|
121
|
+
return full_text if display_width <= width
|
|
122
|
+
|
|
123
|
+
# Otherwise, wrap the input (prompt on first line, continuation indented)
|
|
124
|
+
prompt_width = calculate_display_width(strip_ansi_codes(@prompt))
|
|
125
|
+
available_width = width - prompt_width
|
|
126
|
+
|
|
127
|
+
# For simplicity, just return full text and let terminal handle wrapping
|
|
128
|
+
# InlineInput is typically short, so natural wrapping should be fine
|
|
129
|
+
full_text
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Get cursor column position
|
|
133
|
+
# @return [Integer] Column position
|
|
134
|
+
def cursor_col
|
|
135
|
+
cursor_column(@prompt)
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Deactivate inline input
|
|
139
|
+
def deactivate
|
|
140
|
+
@active = false
|
|
141
|
+
@result_queue = nil
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
private
|
|
145
|
+
|
|
146
|
+
def handle_enter
|
|
147
|
+
result = expand_placeholders(current_line)
|
|
148
|
+
# If empty and has default, use default
|
|
149
|
+
result = @default_value.to_s if result.empty? && @default_value
|
|
150
|
+
|
|
151
|
+
queue = @result_queue
|
|
152
|
+
deactivate
|
|
153
|
+
queue&.push(result)
|
|
154
|
+
|
|
155
|
+
{ action: :submit, result: result }
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def expand_placeholders(text)
|
|
159
|
+
super(text, @paste_placeholders)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def handle_cancel
|
|
163
|
+
queue = @result_queue
|
|
164
|
+
deactivate
|
|
165
|
+
queue&.push(nil)
|
|
166
|
+
|
|
167
|
+
{ action: :cancel }
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def handle_shift_tab
|
|
171
|
+
# Auto-confirm as yes (or use default if it's true)
|
|
172
|
+
result = if @default_value == true || @default_value.to_s.downcase == "yes"
|
|
173
|
+
@default_value.to_s
|
|
174
|
+
else
|
|
175
|
+
"yes"
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
queue = @result_queue
|
|
179
|
+
deactivate
|
|
180
|
+
queue&.push(result)
|
|
181
|
+
|
|
182
|
+
{ action: :toggle_mode }
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|