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.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +43 -0
  3. data/docs/ui2-architecture.md +124 -0
  4. data/lib/clacky/agent.rb +245 -340
  5. data/lib/clacky/agent_config.rb +1 -7
  6. data/lib/clacky/cli.rb +156 -397
  7. data/lib/clacky/client.rb +68 -36
  8. data/lib/clacky/gitignore_parser.rb +26 -12
  9. data/lib/clacky/model_pricing.rb +6 -2
  10. data/lib/clacky/session_manager.rb +6 -2
  11. data/lib/clacky/tools/glob.rb +65 -9
  12. data/lib/clacky/tools/grep.rb +4 -120
  13. data/lib/clacky/tools/run_project.rb +5 -0
  14. data/lib/clacky/tools/safe_shell.rb +49 -13
  15. data/lib/clacky/tools/shell.rb +1 -49
  16. data/lib/clacky/tools/web_fetch.rb +2 -2
  17. data/lib/clacky/tools/web_search.rb +38 -26
  18. data/lib/clacky/ui2/README.md +214 -0
  19. data/lib/clacky/ui2/components/base_component.rb +163 -0
  20. data/lib/clacky/ui2/components/common_component.rb +89 -0
  21. data/lib/clacky/ui2/components/inline_input.rb +187 -0
  22. data/lib/clacky/ui2/components/input_area.rb +1029 -0
  23. data/lib/clacky/ui2/components/message_component.rb +76 -0
  24. data/lib/clacky/ui2/components/output_area.rb +112 -0
  25. data/lib/clacky/ui2/components/todo_area.rb +137 -0
  26. data/lib/clacky/ui2/components/tool_component.rb +106 -0
  27. data/lib/clacky/ui2/components/welcome_banner.rb +93 -0
  28. data/lib/clacky/ui2/layout_manager.rb +331 -0
  29. data/lib/clacky/ui2/line_editor.rb +201 -0
  30. data/lib/clacky/ui2/screen_buffer.rb +238 -0
  31. data/lib/clacky/ui2/theme_manager.rb +68 -0
  32. data/lib/clacky/ui2/themes/base_theme.rb +99 -0
  33. data/lib/clacky/ui2/themes/hacker_theme.rb +56 -0
  34. data/lib/clacky/ui2/themes/minimal_theme.rb +50 -0
  35. data/lib/clacky/ui2/ui_controller.rb +720 -0
  36. data/lib/clacky/ui2/view_renderer.rb +160 -0
  37. data/lib/clacky/ui2.rb +37 -0
  38. data/lib/clacky/utils/file_ignore_helper.rb +126 -0
  39. data/lib/clacky/version.rb +1 -1
  40. data/lib/clacky.rb +1 -6
  41. metadata +38 -6
  42. data/lib/clacky/ui/banner.rb +0 -155
  43. data/lib/clacky/ui/enhanced_prompt.rb +0 -786
  44. data/lib/clacky/ui/formatter.rb +0 -209
  45. data/lib/clacky/ui/statusbar.rb +0 -96
@@ -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, :max_iterations, :max_cost_usd, :timeout_seconds,
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
- require "tty-spinner"
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 :tools, type: :array, default: ["all"], desc: "Allowed tools"
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
- # Always run in interactive mode
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
- def display_recent_messages(messages, limit: 5)
618
- # Filter out user and assistant messages (exclude system and tool messages)
619
- conversation_messages = messages.select { |m| m[:role] == "user" || m[:role] == "assistant" }
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
- # Get the last N messages
622
- recent = conversation_messages.last(limit * 2) # *2 to get user+assistant pairs
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
- if recent.empty?
625
- return
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
- formatter = ui_formatter
629
- formatter.separator("─")
630
- say pastel.dim("Recent conversation history:"), :yellow
631
- formatter.separator("─")
632
-
633
- recent.each do |msg|
634
- case msg[:role]
635
- when "user"
636
- content = truncate_message(msg[:content], 150)
637
- say " #{pastel.blue('[>>]')} You: #{content}"
638
- when "assistant"
639
- content = truncate_message(msg[:content], 200)
640
- say " #{pastel.green('[<<]')} Assistant: #{content}"
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
- formatter.separator("─")
645
- say ""
646
- end
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
- def truncate_message(content, max_length)
649
- return "" if content.nil? || content.empty?
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
- # Remove excessive whitespace
652
- cleaned = content.strip.gsub(/\s+/, ' ')
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
- if cleaned.length > max_length
655
- cleaned[0...max_length] + "..."
656
- else
657
- cleaned
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
- # UI component accessors
662
- def ui_banner
663
- @ui_banner ||= UI::Banner.new
664
- end
402
+ # Initialize UI screen first
403
+ ui_controller.initialize_and_show_banner
665
404
 
666
- def ui_prompt
667
- @ui_prompt ||= UI::EnhancedPrompt.new
668
- end
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
- def ui_statusbar
671
- @ui_statusbar ||= UI::StatusBar.new
672
- end
409
+ begin
410
+ # Set status to working when agent starts
411
+ ui_controller.set_working_status
673
412
 
674
- def ui_formatter
675
- @ui_formatter ||= UI::Formatter.new
676
- end
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
- def pastel
679
- @pastel ||= Pastel.new
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