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
data/lib/clacky/agent_config.rb
CHANGED
|
@@ -5,19 +5,13 @@ module Clacky
|
|
|
5
5
|
PERMISSION_MODES = [:auto_approve, :confirm_safes, :confirm_edits, :plan_only].freeze
|
|
6
6
|
EDITING_TOOLS = %w[write edit].freeze
|
|
7
7
|
|
|
8
|
-
attr_accessor :model, :
|
|
9
|
-
:permission_mode, :allowed_tools, :disallowed_tools,
|
|
8
|
+
attr_accessor :model, :permission_mode,
|
|
10
9
|
:max_tokens, :verbose, :enable_compression, :keep_recent_messages,
|
|
11
10
|
:enable_prompt_caching
|
|
12
11
|
|
|
13
12
|
def initialize(options = {})
|
|
14
13
|
@model = options[:model] || "gpt-3.5-turbo"
|
|
15
|
-
@max_iterations = options[:max_iterations] || 200
|
|
16
|
-
@max_cost_usd = options[:max_cost_usd] || 5.0
|
|
17
|
-
@timeout_seconds = options[:timeout_seconds] # nil means no timeout
|
|
18
14
|
@permission_mode = validate_permission_mode(options[:permission_mode])
|
|
19
|
-
@allowed_tools = options[:allowed_tools]
|
|
20
|
-
@disallowed_tools = options[:disallowed_tools] || []
|
|
21
15
|
@max_tokens = options[:max_tokens] || 8192
|
|
22
16
|
@verbose = options[:verbose] || false
|
|
23
17
|
@enable_compression = options[:enable_compression].nil? ? true : options[:enable_compression]
|
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
|
|
@@ -106,8 +98,7 @@ module Clacky
|
|
|
106
98
|
Dir.chdir(working_dir) if should_chdir
|
|
107
99
|
|
|
108
100
|
begin
|
|
109
|
-
|
|
110
|
-
run_agent_interactive(agent, working_dir, agent_config, message, session_manager, client)
|
|
101
|
+
run_agent_with_ui2(agent, working_dir, agent_config, message, session_manager, client)
|
|
111
102
|
rescue StandardError => e
|
|
112
103
|
# Save session on error
|
|
113
104
|
if session_manager
|
|
@@ -136,9 +127,9 @@ module Clacky
|
|
|
136
127
|
desc "price", "Show pricing information for AI models"
|
|
137
128
|
def price
|
|
138
129
|
say "\n💰 Model Pricing Information\n\n", :green
|
|
139
|
-
|
|
130
|
+
|
|
140
131
|
say "Clacky supports three pricing modes when calculating API costs:\n\n", :white
|
|
141
|
-
|
|
132
|
+
|
|
142
133
|
say " 1. ", :cyan
|
|
143
134
|
say "API-provided cost", :bold
|
|
144
135
|
say " (", :white
|
|
@@ -146,7 +137,7 @@ module Clacky
|
|
|
146
137
|
say ")", :white
|
|
147
138
|
say "\n The most accurate - uses actual cost data from the API response", :white
|
|
148
139
|
say "\n Supported by: OpenRouter, LiteLLM, and other compatible proxies\n\n"
|
|
149
|
-
|
|
140
|
+
|
|
150
141
|
say " 2. ", :cyan
|
|
151
142
|
say "Model-specific pricing", :bold
|
|
152
143
|
say " (", :white
|
|
@@ -154,7 +145,7 @@ module Clacky
|
|
|
154
145
|
say ")", :white
|
|
155
146
|
say "\n Uses official pricing from model providers (Claude models)", :white
|
|
156
147
|
say "\n Includes tiered pricing and prompt caching discounts\n\n"
|
|
157
|
-
|
|
148
|
+
|
|
158
149
|
say " 3. ", :cyan
|
|
159
150
|
say "Default fallback pricing", :bold
|
|
160
151
|
say " (", :white
|
|
@@ -162,9 +153,9 @@ module Clacky
|
|
|
162
153
|
say ")", :white
|
|
163
154
|
say "\n Conservative estimates for unknown models", :white
|
|
164
155
|
say "\n Input: $0.50/MTok, Output: $1.50/MTok\n\n"
|
|
165
|
-
|
|
156
|
+
|
|
166
157
|
say "Priority order: API cost > Model pricing > Default pricing\n\n", :yellow
|
|
167
|
-
|
|
158
|
+
|
|
168
159
|
say "Supported models with official pricing:\n", :green
|
|
169
160
|
say " • claude-opus-4.5\n", :cyan
|
|
170
161
|
say " • claude-sonnet-4.5\n", :cyan
|
|
@@ -172,7 +163,7 @@ module Clacky
|
|
|
172
163
|
say " • claude-3-5-sonnet-20241022\n", :cyan
|
|
173
164
|
say " • claude-3-5-sonnet-20240620\n", :cyan
|
|
174
165
|
say " • claude-3-5-haiku-20241022\n\n", :cyan
|
|
175
|
-
|
|
166
|
+
|
|
176
167
|
say "For detailed pricing information, visit:\n", :white
|
|
177
168
|
say "https://www.anthropic.com/pricing\n\n", :blue
|
|
178
169
|
end
|
|
@@ -182,123 +173,10 @@ module Clacky
|
|
|
182
173
|
AgentConfig.new(
|
|
183
174
|
model: options[:model] || config.model,
|
|
184
175
|
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
176
|
verbose: options[:verbose]
|
|
189
177
|
)
|
|
190
178
|
end
|
|
191
179
|
|
|
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
180
|
def validate_working_directory(path)
|
|
303
181
|
working_dir = path || Dir.pwd
|
|
304
182
|
|
|
@@ -320,218 +198,6 @@ module Clacky
|
|
|
320
198
|
working_dir
|
|
321
199
|
end
|
|
322
200
|
|
|
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
201
|
def list_sessions
|
|
536
202
|
session_manager = Clacky::SessionManager.new
|
|
537
203
|
working_dir = validate_working_directory(options[:path])
|
|
@@ -614,70 +280,163 @@ module Clacky
|
|
|
614
280
|
Clacky::Agent.from_session(client, agent_config, session_data)
|
|
615
281
|
end
|
|
616
282
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
283
|
+
# Handle agent error/interrupt with cleanup
|
|
284
|
+
def handle_agent_exception(ui_controller, agent, session_manager, exception)
|
|
285
|
+
ui_controller.stop_progress_thread
|
|
286
|
+
ui_controller.set_idle_status
|
|
620
287
|
|
|
621
|
-
|
|
622
|
-
|
|
288
|
+
if exception.is_a?(Clacky::AgentInterrupted)
|
|
289
|
+
session_manager&.save(agent.to_session_data(status: :interrupted))
|
|
290
|
+
ui_controller.show_warning("Task interrupted by user")
|
|
291
|
+
else
|
|
292
|
+
error_message = "#{exception.message}\n#{exception.backtrace&.first(3)&.join("\n")}"
|
|
293
|
+
session_manager&.save(agent.to_session_data(status: :error, error_message: error_message))
|
|
294
|
+
ui_controller.show_error("Error: #{exception.message}")
|
|
295
|
+
end
|
|
296
|
+
end
|
|
623
297
|
|
|
624
|
-
|
|
625
|
-
|
|
298
|
+
# Run agent with UI2 split-screen interface
|
|
299
|
+
def run_agent_with_ui2(agent, working_dir, agent_config, initial_message = nil, session_manager = nil, client = nil)
|
|
300
|
+
# Create UI2 controller with configuration
|
|
301
|
+
ui_controller = UI2::UIController.new(
|
|
302
|
+
working_dir: working_dir,
|
|
303
|
+
mode: agent_config.permission_mode.to_s,
|
|
304
|
+
model: agent_config.model
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
# Inject UI into agent
|
|
308
|
+
agent.instance_variable_set(:@ui, ui_controller)
|
|
309
|
+
|
|
310
|
+
# Track agent thread state
|
|
311
|
+
agent_thread = nil
|
|
312
|
+
|
|
313
|
+
# Set up mode toggle handler
|
|
314
|
+
ui_controller.on_mode_toggle do |new_mode|
|
|
315
|
+
agent_config.permission_mode = new_mode.to_sym
|
|
626
316
|
end
|
|
627
317
|
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
318
|
+
# Set up interrupt handler
|
|
319
|
+
ui_controller.on_interrupt do |input_was_empty:|
|
|
320
|
+
if (not agent_thread&.alive?) && input_was_empty
|
|
321
|
+
# Save final session state before exit
|
|
322
|
+
if session_manager && agent.total_tasks > 0
|
|
323
|
+
session_data = agent.to_session_data(status: :exited)
|
|
324
|
+
session_manager.save(session_data)
|
|
325
|
+
|
|
326
|
+
# Show session saved message in output area (before stopping UI)
|
|
327
|
+
session_id = session_data[:session_id][0..7]
|
|
328
|
+
ui_controller.append_output("")
|
|
329
|
+
ui_controller.append_output("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━")
|
|
330
|
+
ui_controller.append_output("")
|
|
331
|
+
ui_controller.append_output("Session saved: #{session_id}")
|
|
332
|
+
ui_controller.append_output("Tasks completed: #{agent.total_tasks}")
|
|
333
|
+
ui_controller.append_output("Total cost: $#{agent.total_cost.round(4)}")
|
|
334
|
+
ui_controller.append_output("")
|
|
335
|
+
ui_controller.append_output("To continue this session, run:")
|
|
336
|
+
ui_controller.append_output(" clacky -a #{session_id}")
|
|
337
|
+
ui_controller.append_output("")
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Stop UI and exit
|
|
341
|
+
ui_controller.stop
|
|
342
|
+
exit(0)
|
|
343
|
+
end
|
|
344
|
+
|
|
345
|
+
if agent_thread&.alive?
|
|
346
|
+
agent_thread.raise(Clacky::AgentInterrupted, "User interrupted")
|
|
641
347
|
end
|
|
348
|
+
ui_controller.input_area.clear
|
|
349
|
+
ui_controller.input_area.set_tips("Press Ctrl+C again to exit.", type: :info)
|
|
642
350
|
end
|
|
643
351
|
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
352
|
+
# Set up input handler
|
|
353
|
+
ui_controller.on_input do |input, images|
|
|
354
|
+
# Handle commands
|
|
355
|
+
case input.downcase.strip
|
|
356
|
+
when "/clear"
|
|
357
|
+
# Clear session by creating a new agent
|
|
358
|
+
agent = Clacky::Agent.new(client, agent_config, working_dir: working_dir, ui: ui_controller)
|
|
359
|
+
ui_controller.show_info("Session cleared. Starting fresh.")
|
|
360
|
+
# Update session bar with reset values
|
|
361
|
+
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
362
|
+
next
|
|
363
|
+
when "/exit", "/quit"
|
|
364
|
+
ui_controller.stop
|
|
365
|
+
exit(0)
|
|
366
|
+
when "/help"
|
|
367
|
+
ui_controller.show_help
|
|
368
|
+
next
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# If agent is already running, interrupt it first
|
|
372
|
+
if agent_thread&.alive?
|
|
373
|
+
agent_thread.raise(Clacky::AgentInterrupted, "New input received")
|
|
374
|
+
agent_thread.join(2) # Wait up to 2 seconds for graceful shutdown
|
|
375
|
+
end
|
|
647
376
|
|
|
648
|
-
|
|
649
|
-
|
|
377
|
+
# Run agent in background thread
|
|
378
|
+
agent_thread = Thread.new do
|
|
379
|
+
begin
|
|
380
|
+
# Set status to working when agent starts
|
|
381
|
+
ui_controller.set_working_status
|
|
650
382
|
|
|
651
|
-
|
|
652
|
-
|
|
383
|
+
# Run agent (Agent will call @ui methods directly)
|
|
384
|
+
# Agent internally tracks total_tasks and total_cost
|
|
385
|
+
result = agent.run(input, images: images)
|
|
653
386
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
387
|
+
# Save session after each task
|
|
388
|
+
if session_manager
|
|
389
|
+
session_manager.save(agent.to_session_data(status: :success))
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
# Update session bar with agent's cumulative stats
|
|
393
|
+
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
394
|
+
rescue Clacky::AgentInterrupted, StandardError => e
|
|
395
|
+
handle_agent_exception(ui_controller, agent, session_manager, e)
|
|
396
|
+
ensure
|
|
397
|
+
agent_thread = nil
|
|
398
|
+
end
|
|
399
|
+
end
|
|
658
400
|
end
|
|
659
|
-
end
|
|
660
401
|
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
@ui_banner ||= UI::Banner.new
|
|
664
|
-
end
|
|
402
|
+
# Initialize UI screen first
|
|
403
|
+
ui_controller.initialize_and_show_banner
|
|
665
404
|
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
405
|
+
# If there's an initial message, process it
|
|
406
|
+
if initial_message && !initial_message.strip.empty?
|
|
407
|
+
ui_controller.show_user_message(initial_message)
|
|
669
408
|
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
409
|
+
begin
|
|
410
|
+
# Set status to working when agent starts
|
|
411
|
+
ui_controller.set_working_status
|
|
673
412
|
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
413
|
+
result = agent.run(initial_message, images: [])
|
|
414
|
+
|
|
415
|
+
if session_manager
|
|
416
|
+
session_manager.save(agent.to_session_data(status: :success))
|
|
417
|
+
end
|
|
677
418
|
|
|
678
|
-
|
|
679
|
-
|
|
419
|
+
# Update session bar with agent's cumulative stats
|
|
420
|
+
ui_controller.update_sessionbar(tasks: agent.total_tasks, cost: agent.total_cost)
|
|
421
|
+
rescue Clacky::AgentInterrupted, StandardError => e
|
|
422
|
+
handle_agent_exception(ui_controller, agent, session_manager, e)
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
# Start input loop (blocks until exit)
|
|
427
|
+
ui_controller.start_input_loop
|
|
428
|
+
|
|
429
|
+
# Save final session state
|
|
430
|
+
if session_manager && agent.total_tasks > 0
|
|
431
|
+
session_manager.save(agent.to_session_data)
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
# Show goodbye message
|
|
435
|
+
say "\n👋 Goodbye! Session stats:", :green
|
|
436
|
+
say " Tasks completed: #{agent.total_tasks}", :cyan
|
|
437
|
+
say " Total cost: $#{agent.total_cost.round(4)}", :cyan
|
|
680
438
|
end
|
|
439
|
+
|
|
681
440
|
end
|
|
682
441
|
end
|
|
683
442
|
|