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
data/lib/clacky/cli.rb
CHANGED
|
@@ -2,11 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
require "thor"
|
|
4
4
|
require "tty-prompt"
|
|
5
|
-
|
|
6
|
-
require_relative "ui/banner"
|
|
7
|
-
require_relative "ui/enhanced_prompt"
|
|
8
|
-
require_relative "ui/statusbar"
|
|
9
|
-
require_relative "ui/formatter"
|
|
5
|
+
require_relative "ui2"
|
|
10
6
|
|
|
11
7
|
module Clacky
|
|
12
8
|
class CLI < Thor
|
|
@@ -42,26 +38,22 @@ module Clacky
|
|
|
42
38
|
-a, --attach N - Attach to session by number (e.g., -a 2) or session ID prefix (e.g., -a b6682a87)
|
|
43
39
|
|
|
44
40
|
Examples:
|
|
45
|
-
$ clacky agent
|
|
46
|
-
$ clacky agent "Create a README file"
|
|
47
41
|
$ clacky agent --mode=auto_approve --path /path/to/project
|
|
48
|
-
$ clacky agent --tools file_reader glob grep
|
|
49
|
-
$ clacky agent -c
|
|
50
|
-
$ clacky agent -l
|
|
51
|
-
$ clacky agent -a 2
|
|
52
|
-
$ clacky agent -a b6682a87
|
|
53
42
|
LONGDESC
|
|
54
43
|
option :mode, type: :string, default: "confirm_safes",
|
|
55
44
|
desc: "Permission mode: auto_approve, confirm_safes, confirm_edits, plan_only"
|
|
56
|
-
option :
|
|
57
|
-
option :max_iterations, type: :numeric, desc: "Maximum iterations (default: 50)"
|
|
58
|
-
option :max_cost, type: :numeric, desc: "Maximum cost in USD (default: 5.0)"
|
|
59
|
-
option :verbose, type: :boolean, default: false, desc: "Show detailed output"
|
|
45
|
+
option :verbose, type: :boolean, aliases: "-v", default: false, desc: "Show detailed output"
|
|
60
46
|
option :path, type: :string, desc: "Project directory path (defaults to current directory)"
|
|
61
47
|
option :continue, type: :boolean, aliases: "-c", desc: "Continue most recent session"
|
|
62
48
|
option :list, type: :boolean, aliases: "-l", desc: "List recent sessions"
|
|
63
49
|
option :attach, type: :string, aliases: "-a", desc: "Attach to session by number or keyword"
|
|
50
|
+
option :help, type: :boolean, aliases: "-h", desc: "Show this help message"
|
|
64
51
|
def agent(message = nil)
|
|
52
|
+
# Handle help option
|
|
53
|
+
if options[:help]
|
|
54
|
+
invoke :help, ["agent"]
|
|
55
|
+
return
|
|
56
|
+
end
|
|
65
57
|
config = Clacky::Config.load
|
|
66
58
|
|
|
67
59
|
unless config.api_key
|
|
@@ -90,11 +82,14 @@ module Clacky
|
|
|
90
82
|
# Handle session loading/continuation
|
|
91
83
|
session_manager = Clacky::SessionManager.new
|
|
92
84
|
agent = nil
|
|
85
|
+
is_session_load = false
|
|
93
86
|
|
|
94
87
|
if options[:continue]
|
|
95
88
|
agent = load_latest_session(client, agent_config, session_manager, working_dir)
|
|
89
|
+
is_session_load = !agent.nil?
|
|
96
90
|
elsif options[:attach]
|
|
97
91
|
agent = load_session_by_number(client, agent_config, session_manager, working_dir, options[:attach])
|
|
92
|
+
is_session_load = !agent.nil?
|
|
98
93
|
end
|
|
99
94
|
|
|
100
95
|
# Create new agent if no session loaded
|
|
@@ -106,8 +101,7 @@ module Clacky
|
|
|
106
101
|
Dir.chdir(working_dir) if should_chdir
|
|
107
102
|
|
|
108
103
|
begin
|
|
109
|
-
|
|
110
|
-
run_agent_interactive(agent, working_dir, agent_config, message, session_manager, client)
|
|
104
|
+
run_agent_with_ui2(agent, working_dir, agent_config, message, session_manager, client, is_session_load: is_session_load)
|
|
111
105
|
rescue StandardError => e
|
|
112
106
|
# Save session on error
|
|
113
107
|
if session_manager
|
|
@@ -136,9 +130,9 @@ module Clacky
|
|
|
136
130
|
desc "price", "Show pricing information for AI models"
|
|
137
131
|
def price
|
|
138
132
|
say "\n💰 Model Pricing Information\n\n", :green
|
|
139
|
-
|
|
133
|
+
|
|
140
134
|
say "Clacky supports three pricing modes when calculating API costs:\n\n", :white
|
|
141
|
-
|
|
135
|
+
|
|
142
136
|
say " 1. ", :cyan
|
|
143
137
|
say "API-provided cost", :bold
|
|
144
138
|
say " (", :white
|
|
@@ -146,7 +140,7 @@ module Clacky
|
|
|
146
140
|
say ")", :white
|
|
147
141
|
say "\n The most accurate - uses actual cost data from the API response", :white
|
|
148
142
|
say "\n Supported by: OpenRouter, LiteLLM, and other compatible proxies\n\n"
|
|
149
|
-
|
|
143
|
+
|
|
150
144
|
say " 2. ", :cyan
|
|
151
145
|
say "Model-specific pricing", :bold
|
|
152
146
|
say " (", :white
|
|
@@ -154,7 +148,7 @@ module Clacky
|
|
|
154
148
|
say ")", :white
|
|
155
149
|
say "\n Uses official pricing from model providers (Claude models)", :white
|
|
156
150
|
say "\n Includes tiered pricing and prompt caching discounts\n\n"
|
|
157
|
-
|
|
151
|
+
|
|
158
152
|
say " 3. ", :cyan
|
|
159
153
|
say "Default fallback pricing", :bold
|
|
160
154
|
say " (", :white
|
|
@@ -162,9 +156,9 @@ module Clacky
|
|
|
162
156
|
say ")", :white
|
|
163
157
|
say "\n Conservative estimates for unknown models", :white
|
|
164
158
|
say "\n Input: $0.50/MTok, Output: $1.50/MTok\n\n"
|
|
165
|
-
|
|
159
|
+
|
|
166
160
|
say "Priority order: API cost > Model pricing > Default pricing\n\n", :yellow
|
|
167
|
-
|
|
161
|
+
|
|
168
162
|
say "Supported models with official pricing:\n", :green
|
|
169
163
|
say " • claude-opus-4.5\n", :cyan
|
|
170
164
|
say " • claude-sonnet-4.5\n", :cyan
|
|
@@ -172,7 +166,7 @@ module Clacky
|
|
|
172
166
|
say " • claude-3-5-sonnet-20241022\n", :cyan
|
|
173
167
|
say " • claude-3-5-sonnet-20240620\n", :cyan
|
|
174
168
|
say " • claude-3-5-haiku-20241022\n\n", :cyan
|
|
175
|
-
|
|
169
|
+
|
|
176
170
|
say "For detailed pricing information, visit:\n", :white
|
|
177
171
|
say "https://www.anthropic.com/pricing\n\n", :blue
|
|
178
172
|
end
|
|
@@ -182,123 +176,10 @@ module Clacky
|
|
|
182
176
|
AgentConfig.new(
|
|
183
177
|
model: options[:model] || config.model,
|
|
184
178
|
permission_mode: options[:mode].to_sym,
|
|
185
|
-
allowed_tools: options[:tools],
|
|
186
|
-
max_iterations: options[:max_iterations],
|
|
187
|
-
max_cost_usd: options[:max_cost],
|
|
188
179
|
verbose: options[:verbose]
|
|
189
180
|
)
|
|
190
181
|
end
|
|
191
182
|
|
|
192
|
-
def prompt_for_input
|
|
193
|
-
prompt = TTY::Prompt.new
|
|
194
|
-
prompt.ask("What would you like the agent to do?", required: true)
|
|
195
|
-
end
|
|
196
|
-
|
|
197
|
-
def display_agent_event(event)
|
|
198
|
-
formatter = ui_formatter
|
|
199
|
-
|
|
200
|
-
case event[:type]
|
|
201
|
-
when :thinking
|
|
202
|
-
formatter.thinking
|
|
203
|
-
when :assistant_message
|
|
204
|
-
# Display assistant's thinking/explanation before tool calls
|
|
205
|
-
formatter.assistant_message(event[:data][:content])
|
|
206
|
-
when :tool_call
|
|
207
|
-
display_tool_call(event[:data])
|
|
208
|
-
when :observation
|
|
209
|
-
display_tool_result(event[:data])
|
|
210
|
-
# Auto-display TODO status if exists
|
|
211
|
-
display_todo_status_if_exists
|
|
212
|
-
when :answer
|
|
213
|
-
formatter.assistant_message(event[:data][:content])
|
|
214
|
-
when :tool_denied
|
|
215
|
-
formatter.tool_denied(event[:data][:name])
|
|
216
|
-
when :tool_planned
|
|
217
|
-
formatter.tool_planned(event[:data][:name])
|
|
218
|
-
when :tool_error
|
|
219
|
-
formatter.tool_error(event[:data][:error].message)
|
|
220
|
-
when :on_iteration
|
|
221
|
-
formatter.iteration(event[:data][:iteration]) if options[:verbose]
|
|
222
|
-
end
|
|
223
|
-
end
|
|
224
|
-
|
|
225
|
-
def display_tool_call(data)
|
|
226
|
-
tool_name = data[:name]
|
|
227
|
-
args_json = data[:arguments]
|
|
228
|
-
|
|
229
|
-
# Get tool instance to use its format_call method
|
|
230
|
-
tool = get_tool_instance(tool_name)
|
|
231
|
-
if tool
|
|
232
|
-
begin
|
|
233
|
-
args = JSON.parse(args_json, symbolize_names: true)
|
|
234
|
-
formatted = tool.format_call(args)
|
|
235
|
-
ui_formatter.tool_call(formatted)
|
|
236
|
-
rescue JSON::ParserError, StandardError => e
|
|
237
|
-
say "⚠️ Warning: Failed to format tool call: #{e.message}", :yellow
|
|
238
|
-
ui_formatter.tool_call("#{tool_name}(...)")
|
|
239
|
-
end
|
|
240
|
-
else
|
|
241
|
-
say "⚠️ Warning: Tool instance not found for '#{tool_name}'", :yellow
|
|
242
|
-
ui_formatter.tool_call("#{tool_name}(...)")
|
|
243
|
-
end
|
|
244
|
-
|
|
245
|
-
# Show verbose details if requested
|
|
246
|
-
if options[:verbose]
|
|
247
|
-
say " Arguments: #{args_json[0..200]}", :white
|
|
248
|
-
end
|
|
249
|
-
end
|
|
250
|
-
|
|
251
|
-
def display_tool_result(data)
|
|
252
|
-
tool_name = data[:tool]
|
|
253
|
-
result = data[:result]
|
|
254
|
-
|
|
255
|
-
# Get tool instance to use its format_result method
|
|
256
|
-
tool = get_tool_instance(tool_name)
|
|
257
|
-
if tool
|
|
258
|
-
begin
|
|
259
|
-
summary = tool.format_result(result)
|
|
260
|
-
ui_formatter.tool_result(summary)
|
|
261
|
-
rescue StandardError => e
|
|
262
|
-
ui_formatter.tool_result("Done")
|
|
263
|
-
end
|
|
264
|
-
else
|
|
265
|
-
# Fallback for unknown tools
|
|
266
|
-
result_str = result.to_s
|
|
267
|
-
summary = result_str.length > 100 ? "#{result_str[0..100]}..." : result_str
|
|
268
|
-
ui_formatter.tool_result(summary)
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
# Show verbose details if requested
|
|
272
|
-
if options[:verbose] && result.is_a?(Hash)
|
|
273
|
-
say " #{result.inspect[0..200]}", :white
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
def get_tool_instance(tool_name)
|
|
278
|
-
# Use metaprogramming to find tool class by name
|
|
279
|
-
# Convert tool_name to class name (e.g., "file_reader" -> "FileReader")
|
|
280
|
-
class_name = tool_name.split('_').map(&:capitalize).join
|
|
281
|
-
|
|
282
|
-
# Try to find the class in Clacky::Tools namespace
|
|
283
|
-
if Clacky::Tools.const_defined?(class_name)
|
|
284
|
-
tool_class = Clacky::Tools.const_get(class_name)
|
|
285
|
-
tool_class.new
|
|
286
|
-
else
|
|
287
|
-
nil
|
|
288
|
-
end
|
|
289
|
-
rescue NameError
|
|
290
|
-
nil
|
|
291
|
-
end
|
|
292
|
-
|
|
293
|
-
def display_todo_status_if_exists
|
|
294
|
-
return unless @current_agent
|
|
295
|
-
|
|
296
|
-
todos = @current_agent.todos
|
|
297
|
-
return if todos.empty?
|
|
298
|
-
|
|
299
|
-
ui_formatter.todo_status(todos)
|
|
300
|
-
end
|
|
301
|
-
|
|
302
183
|
def validate_working_directory(path)
|
|
303
184
|
working_dir = path || Dir.pwd
|
|
304
185
|
|
|
@@ -320,218 +201,6 @@ module Clacky
|
|
|
320
201
|
working_dir
|
|
321
202
|
end
|
|
322
203
|
|
|
323
|
-
def run_in_directory(directory)
|
|
324
|
-
original_dir = Dir.pwd
|
|
325
|
-
|
|
326
|
-
begin
|
|
327
|
-
Dir.chdir(directory)
|
|
328
|
-
yield
|
|
329
|
-
ensure
|
|
330
|
-
Dir.chdir(original_dir)
|
|
331
|
-
end
|
|
332
|
-
end
|
|
333
|
-
|
|
334
|
-
def run_agent_interactive(agent, working_dir, agent_config, initial_message = nil, session_manager = nil, client = nil)
|
|
335
|
-
# Store agent as instance variable for access in display methods
|
|
336
|
-
@current_agent = agent
|
|
337
|
-
|
|
338
|
-
# Initialize UI components
|
|
339
|
-
banner = ui_banner
|
|
340
|
-
prompt = ui_prompt
|
|
341
|
-
statusbar = ui_statusbar
|
|
342
|
-
|
|
343
|
-
# Show startup banner for new session
|
|
344
|
-
if agent.total_tasks == 0
|
|
345
|
-
banner.display_startup
|
|
346
|
-
end
|
|
347
|
-
|
|
348
|
-
# Show session info if continuing
|
|
349
|
-
if agent.total_tasks > 0
|
|
350
|
-
banner.display_session_continue(
|
|
351
|
-
session_id: agent.session_id[0..7],
|
|
352
|
-
created_at: Time.parse(agent.created_at).strftime('%Y-%m-%d %H:%M'),
|
|
353
|
-
tasks: agent.total_tasks,
|
|
354
|
-
cost: agent.total_cost.round(4)
|
|
355
|
-
)
|
|
356
|
-
|
|
357
|
-
# Show recent conversation history
|
|
358
|
-
display_recent_messages(agent.messages, limit: 5)
|
|
359
|
-
else
|
|
360
|
-
# Show welcome info for new session
|
|
361
|
-
banner.display_agent_welcome(
|
|
362
|
-
working_dir: working_dir,
|
|
363
|
-
mode: agent_config.permission_mode,
|
|
364
|
-
max_iterations: agent_config.max_iterations,
|
|
365
|
-
max_cost: agent_config.max_cost_usd
|
|
366
|
-
)
|
|
367
|
-
end
|
|
368
|
-
|
|
369
|
-
total_tasks = agent.total_tasks
|
|
370
|
-
total_cost = agent.total_cost
|
|
371
|
-
|
|
372
|
-
# Process initial message if provided
|
|
373
|
-
current_message = initial_message
|
|
374
|
-
current_images = []
|
|
375
|
-
|
|
376
|
-
loop do
|
|
377
|
-
# Get message from user if not provided
|
|
378
|
-
unless current_message && !current_message.strip.empty?
|
|
379
|
-
# Only show newline separator if we've completed tasks
|
|
380
|
-
# (but not right after /clear since we just showed a message)
|
|
381
|
-
say "\n" if total_tasks > 0
|
|
382
|
-
|
|
383
|
-
# Show status bar before input
|
|
384
|
-
statusbar.display(
|
|
385
|
-
working_dir: working_dir,
|
|
386
|
-
mode: agent_config.permission_mode.to_s,
|
|
387
|
-
model: agent_config.model,
|
|
388
|
-
tasks: total_tasks,
|
|
389
|
-
cost: total_cost
|
|
390
|
-
)
|
|
391
|
-
|
|
392
|
-
# Use enhanced prompt with "❯" prefix
|
|
393
|
-
result = prompt.read_input(prefix: "❯") do |display_lines|
|
|
394
|
-
# Shift+Tab pressed - toggle mode and update status bar
|
|
395
|
-
if agent_config.permission_mode == :confirm_safes
|
|
396
|
-
agent_config.permission_mode = :auto_approve
|
|
397
|
-
else
|
|
398
|
-
agent_config.permission_mode = :confirm_safes
|
|
399
|
-
end
|
|
400
|
-
|
|
401
|
-
# Update status bar (it's above the input box)
|
|
402
|
-
# display_lines includes the final newline, so we need display_lines moves to reach status bar
|
|
403
|
-
print "\e[#{display_lines}A" # Move up to status bar line
|
|
404
|
-
print "\r\e[2K" # Clear the status bar line
|
|
405
|
-
|
|
406
|
-
# Redisplay status bar with new mode (puts adds newline, cursor moves to next line)
|
|
407
|
-
statusbar.display(
|
|
408
|
-
working_dir: working_dir,
|
|
409
|
-
mode: agent_config.permission_mode.to_s,
|
|
410
|
-
model: agent_config.model,
|
|
411
|
-
tasks: total_tasks,
|
|
412
|
-
cost: total_cost
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
# Move back down to original position (display_lines - 1 because puts moved us down 1)
|
|
416
|
-
print "\e[#{display_lines - 1}B"
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
# EnhancedPrompt returns:
|
|
420
|
-
# - { text: String, images: Array } for normal input
|
|
421
|
-
# - { command: Symbol } for commands
|
|
422
|
-
# - nil on EOF
|
|
423
|
-
if result.nil?
|
|
424
|
-
current_message = nil
|
|
425
|
-
current_images = []
|
|
426
|
-
break
|
|
427
|
-
elsif result[:command]
|
|
428
|
-
# Handle commands
|
|
429
|
-
case result[:command]
|
|
430
|
-
when :clear
|
|
431
|
-
# Clear session by creating a new agent
|
|
432
|
-
agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir)
|
|
433
|
-
@current_agent = agent
|
|
434
|
-
total_tasks = 0
|
|
435
|
-
total_cost = 0.0
|
|
436
|
-
ui_formatter.info("Session cleared. Starting fresh.")
|
|
437
|
-
current_message = nil
|
|
438
|
-
current_images = []
|
|
439
|
-
next
|
|
440
|
-
when :exit
|
|
441
|
-
current_message = nil
|
|
442
|
-
current_images = []
|
|
443
|
-
break
|
|
444
|
-
end
|
|
445
|
-
else
|
|
446
|
-
# Normal input with text and optional images
|
|
447
|
-
current_message = result[:text]
|
|
448
|
-
current_images = result[:images] || []
|
|
449
|
-
end
|
|
450
|
-
|
|
451
|
-
break if current_message.nil? || %w[exit quit].include?(current_message&.downcase&.strip)
|
|
452
|
-
next if current_message.strip.empty? && current_images.empty?
|
|
453
|
-
|
|
454
|
-
# Display user's message after input
|
|
455
|
-
ui_formatter.user_message(current_message)
|
|
456
|
-
|
|
457
|
-
# Display image info if images were pasted (without extra newline)
|
|
458
|
-
if current_images.any?
|
|
459
|
-
current_images.each_with_index do |img_path, idx|
|
|
460
|
-
filename = File.basename(img_path)
|
|
461
|
-
say " 📎 Image #{idx + 1}: #{filename}", :cyan
|
|
462
|
-
end
|
|
463
|
-
puts # Add newline after all images
|
|
464
|
-
else
|
|
465
|
-
puts # Add newline after user message if no images
|
|
466
|
-
end
|
|
467
|
-
end
|
|
468
|
-
|
|
469
|
-
total_tasks += 1
|
|
470
|
-
|
|
471
|
-
begin
|
|
472
|
-
result = agent.run(current_message, images: current_images) do |event|
|
|
473
|
-
display_agent_event(event)
|
|
474
|
-
end
|
|
475
|
-
|
|
476
|
-
total_cost += result[:total_cost_usd]
|
|
477
|
-
|
|
478
|
-
# Save session after each task with success status
|
|
479
|
-
if session_manager
|
|
480
|
-
session_manager.save(agent.to_session_data(status: :success))
|
|
481
|
-
end
|
|
482
|
-
|
|
483
|
-
# Show brief task completion
|
|
484
|
-
banner.display_task_complete(
|
|
485
|
-
iterations: result[:iterations],
|
|
486
|
-
cost: result[:total_cost_usd].round(4),
|
|
487
|
-
total_tasks: total_tasks,
|
|
488
|
-
total_cost: total_cost.round(4),
|
|
489
|
-
cost_source: result[:cost_source],
|
|
490
|
-
cache_stats: result[:cache_stats]
|
|
491
|
-
)
|
|
492
|
-
rescue Clacky::AgentInterrupted
|
|
493
|
-
# Save session on interruption
|
|
494
|
-
if session_manager
|
|
495
|
-
session_manager.save(agent.to_session_data(status: :interrupted))
|
|
496
|
-
ui_formatter.warning("Task interrupted by user (Ctrl+C)")
|
|
497
|
-
say "You can start a new task or type 'exit' to quit.\n", :yellow
|
|
498
|
-
end
|
|
499
|
-
rescue StandardError => e
|
|
500
|
-
# Save session on error
|
|
501
|
-
if session_manager
|
|
502
|
-
session_manager.save(agent.to_session_data(status: :error, error_message: e.message))
|
|
503
|
-
end
|
|
504
|
-
|
|
505
|
-
# Report the error
|
|
506
|
-
banner.display_error(e.message, details: options[:verbose] ? e.backtrace.first(3).join("\n") : nil)
|
|
507
|
-
|
|
508
|
-
# Show session saved message
|
|
509
|
-
if session_manager&.last_saved_path
|
|
510
|
-
ui_formatter.info("Session saved: #{session_manager.last_saved_path}")
|
|
511
|
-
end
|
|
512
|
-
|
|
513
|
-
# Guide user to recover
|
|
514
|
-
ui_formatter.info("To recover and retry, run: clacky agent -c")
|
|
515
|
-
say "\nOr you can continue with a new task or type 'exit' to quit.", :yellow
|
|
516
|
-
end
|
|
517
|
-
|
|
518
|
-
# Clear current_message and current_images to prompt for next input
|
|
519
|
-
current_message = nil
|
|
520
|
-
current_images = []
|
|
521
|
-
end
|
|
522
|
-
|
|
523
|
-
# Save final session state only if there were actual tasks
|
|
524
|
-
# Don't save empty sessions where user just started and exited
|
|
525
|
-
if session_manager && total_tasks > 0
|
|
526
|
-
session_manager.save(agent.to_session_data)
|
|
527
|
-
end
|
|
528
|
-
|
|
529
|
-
banner.display_goodbye(
|
|
530
|
-
total_tasks: total_tasks,
|
|
531
|
-
total_cost: total_cost.round(4)
|
|
532
|
-
)
|
|
533
|
-
end
|
|
534
|
-
|
|
535
204
|
def list_sessions
|
|
536
205
|
session_manager = Clacky::SessionManager.new
|
|
537
206
|
working_dir = validate_working_directory(options[:path])
|
|
@@ -565,7 +234,7 @@ module Clacky
|
|
|
565
234
|
return nil
|
|
566
235
|
end
|
|
567
236
|
|
|
568
|
-
|
|
237
|
+
# Don't print message here - will be shown by UI after banner
|
|
569
238
|
Clacky::Agent.from_session(client, agent_config, session_data)
|
|
570
239
|
end
|
|
571
240
|
|
|
@@ -610,74 +279,174 @@ module Clacky
|
|
|
610
279
|
end
|
|
611
280
|
end
|
|
612
281
|
|
|
613
|
-
|
|
282
|
+
# Don't print message here - will be shown by UI after banner
|
|
614
283
|
Clacky::Agent.from_session(client, agent_config, session_data)
|
|
615
284
|
end
|
|
616
285
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
286
|
+
# Handle agent error/interrupt with cleanup
|
|
287
|
+
def handle_agent_exception(ui_controller, agent, session_manager, exception)
|
|
288
|
+
ui_controller.stop_progress_thread
|
|
289
|
+
ui_controller.set_idle_status
|
|
620
290
|
|
|
621
|
-
|
|
622
|
-
|
|
291
|
+
if exception.is_a?(Clacky::AgentInterrupted)
|
|
292
|
+
session_manager&.save(agent.to_session_data(status: :interrupted))
|
|
293
|
+
ui_controller.show_warning("Task interrupted by user")
|
|
294
|
+
else
|
|
295
|
+
error_message = "#{exception.message}\n#{exception.backtrace&.first(3)&.join("\n")}"
|
|
296
|
+
session_manager&.save(agent.to_session_data(status: :error, error_message: error_message))
|
|
297
|
+
ui_controller.show_error("Error: #{exception.message}")
|
|
298
|
+
end
|
|
299
|
+
end
|
|
623
300
|
|
|
624
|
-
|
|
625
|
-
|
|
301
|
+
# Run agent with UI2 split-screen interface
|
|
302
|
+
def run_agent_with_ui2(agent, working_dir, agent_config, initial_message = nil, session_manager = nil, client = nil, is_session_load: false)
|
|
303
|
+
# Create UI2 controller with configuration
|
|
304
|
+
ui_controller = UI2::UIController.new(
|
|
305
|
+
working_dir: working_dir,
|
|
306
|
+
mode: agent_config.permission_mode.to_s,
|
|
307
|
+
model: agent_config.model
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
# Inject UI into agent
|
|
311
|
+
agent.instance_variable_set(:@ui, ui_controller)
|
|
312
|
+
|
|
313
|
+
# Track agent thread state
|
|
314
|
+
agent_thread = nil
|
|
315
|
+
|
|
316
|
+
# Set up mode toggle handler
|
|
317
|
+
ui_controller.on_mode_toggle do |new_mode|
|
|
318
|
+
agent_config.permission_mode = new_mode.to_sym
|
|
626
319
|
end
|
|
627
320
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
321
|
+
# Set up interrupt handler
|
|
322
|
+
ui_controller.on_interrupt do |input_was_empty:|
|
|
323
|
+
if (not agent_thread&.alive?) && input_was_empty
|
|
324
|
+
# Save final session state before exit
|
|
325
|
+
if session_manager && agent.total_tasks > 0
|
|
326
|
+
session_data = agent.to_session_data(status: :exited)
|
|
327
|
+
session_manager.save(session_data)
|
|
328
|
+
|
|
329
|
+
# Show session saved message in output area (before stopping UI)
|
|
330
|
+
session_id = session_data[:session_id][0..7]
|
|
331
|
+
ui_controller.append_output("")
|
|
332
|
+
ui_controller.append_output("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
333
|
+
ui_controller.append_output("")
|
|
334
|
+
ui_controller.append_output("Session saved: #{session_id}")
|
|
335
|
+
ui_controller.append_output("Tasks completed: #{agent.total_tasks}")
|
|
336
|
+
ui_controller.append_output("Total cost: $#{agent.total_cost.round(4)}")
|
|
337
|
+
ui_controller.append_output("")
|
|
338
|
+
ui_controller.append_output("To continue this session, run:")
|
|
339
|
+
ui_controller.append_output(" clacky -a #{session_id}")
|
|
340
|
+
ui_controller.append_output("")
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
# Stop UI and exit
|
|
344
|
+
ui_controller.stop
|
|
345
|
+
exit(0)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
if agent_thread&.alive?
|
|
349
|
+
agent_thread.raise(Clacky::AgentInterrupted, "User interrupted")
|
|
641
350
|
end
|
|
351
|
+
ui_controller.input_area.clear
|
|
352
|
+
ui_controller.input_area.set_tips("Press Ctrl+C again to exit.", type: :info)
|
|
642
353
|
end
|
|
643
354
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
355
|
+
# Set up input handler
|
|
356
|
+
ui_controller.on_input do |input, images|
|
|
357
|
+
# Handle commands
|
|
358
|
+
case input.downcase.strip
|
|
359
|
+
when "/clear"
|
|
360
|
+
# Clear session by creating a new agent
|
|
361
|
+
agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: ui_controller)
|
|
362
|
+
ui_controller.show_info("Session cleared. Starting fresh.")
|
|
363
|
+
# Update session bar with reset values
|
|
364
|
+
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
365
|
+
# Clear todo area display
|
|
366
|
+
ui_controller.update_todos([])
|
|
367
|
+
next
|
|
368
|
+
when "/exit", "/quit"
|
|
369
|
+
ui_controller.stop
|
|
370
|
+
exit(0)
|
|
371
|
+
when "/help"
|
|
372
|
+
ui_controller.show_help
|
|
373
|
+
next
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
# If agent is already running, interrupt it first
|
|
377
|
+
if agent_thread&.alive?
|
|
378
|
+
agent_thread.raise(Clacky::AgentInterrupted, "New input received")
|
|
379
|
+
agent_thread.join(2) # Wait up to 2 seconds for graceful shutdown
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
# Run agent in background thread
|
|
383
|
+
agent_thread = Thread.new do
|
|
384
|
+
begin
|
|
385
|
+
# Set status to working when agent starts
|
|
386
|
+
ui_controller.set_working_status
|
|
647
387
|
|
|
648
|
-
|
|
649
|
-
|
|
388
|
+
# Run agent (Agent will call @ui methods directly)
|
|
389
|
+
# Agent internally tracks total_tasks and total_cost
|
|
390
|
+
result = agent.run(input, images: images)
|
|
391
|
+
|
|
392
|
+
# Save session after each task
|
|
393
|
+
if session_manager
|
|
394
|
+
session_manager.save(agent.to_session_data(status: :success))
|
|
395
|
+
end
|
|
650
396
|
|
|
651
|
-
|
|
652
|
-
|
|
397
|
+
# Update session bar with agent's cumulative stats
|
|
398
|
+
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
399
|
+
rescue Clacky::AgentInterrupted, StandardError => e
|
|
400
|
+
handle_agent_exception(ui_controller, agent, session_manager, e)
|
|
401
|
+
ensure
|
|
402
|
+
agent_thread = nil
|
|
403
|
+
end
|
|
404
|
+
end
|
|
405
|
+
end
|
|
653
406
|
|
|
654
|
-
|
|
655
|
-
|
|
407
|
+
# Initialize UI screen first
|
|
408
|
+
if is_session_load
|
|
409
|
+
recent_user_messages = agent.get_recent_user_messages(limit: 5)
|
|
410
|
+
ui_controller.initialize_and_show_banner(recent_user_messages: recent_user_messages)
|
|
656
411
|
else
|
|
657
|
-
|
|
412
|
+
ui_controller.initialize_and_show_banner
|
|
658
413
|
end
|
|
659
|
-
end
|
|
660
414
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
end
|
|
415
|
+
# If there's an initial message, process it
|
|
416
|
+
if initial_message && !initial_message.strip.empty?
|
|
417
|
+
ui_controller.show_user_message(initial_message)
|
|
665
418
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
419
|
+
begin
|
|
420
|
+
# Set status to working when agent starts
|
|
421
|
+
ui_controller.set_working_status
|
|
669
422
|
|
|
670
|
-
|
|
671
|
-
@ui_statusbar ||= UI::StatusBar.new
|
|
672
|
-
end
|
|
423
|
+
result = agent.run(initial_message, images: [])
|
|
673
424
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
425
|
+
if session_manager
|
|
426
|
+
session_manager.save(agent.to_session_data(status: :success))
|
|
427
|
+
end
|
|
677
428
|
|
|
678
|
-
|
|
679
|
-
|
|
429
|
+
# Update session bar with agent's cumulative stats
|
|
430
|
+
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
431
|
+
rescue Clacky::AgentInterrupted, StandardError => e
|
|
432
|
+
handle_agent_exception(ui_controller, agent, session_manager, e)
|
|
433
|
+
end
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Start input loop (blocks until exit)
|
|
437
|
+
ui_controller.start_input_loop
|
|
438
|
+
|
|
439
|
+
# Save final session state
|
|
440
|
+
if session_manager && agent.total_tasks > 0
|
|
441
|
+
session_manager.save(agent.to_session_data)
|
|
442
|
+
end
|
|
443
|
+
|
|
444
|
+
# Show goodbye message
|
|
445
|
+
say "\n👋 Goodbye! Session stats:", :green
|
|
446
|
+
say " Tasks completed: #{agent.total_tasks}", :cyan
|
|
447
|
+
say " Total cost: $#{agent.total_cost.round(4)}", :cyan
|
|
680
448
|
end
|
|
449
|
+
|
|
681
450
|
end
|
|
682
451
|
end
|
|
683
452
|
|