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.
Files changed (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +71 -0
  3. data/docs/ui2-architecture.md +124 -0
  4. data/lib/clacky/agent.rb +376 -346
  5. data/lib/clacky/agent_config.rb +1 -7
  6. data/lib/clacky/cli.rb +167 -398
  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 +66 -10
  12. data/lib/clacky/tools/grep.rb +6 -122
  13. data/lib/clacky/tools/run_project.rb +10 -5
  14. data/lib/clacky/tools/safe_shell.rb +149 -20
  15. data/lib/clacky/tools/shell.rb +3 -51
  16. data/lib/clacky/tools/todo_manager.rb +50 -3
  17. data/lib/clacky/tools/trash_manager.rb +1 -1
  18. data/lib/clacky/tools/web_fetch.rb +4 -4
  19. data/lib/clacky/tools/web_search.rb +40 -28
  20. data/lib/clacky/ui2/README.md +214 -0
  21. data/lib/clacky/ui2/components/base_component.rb +163 -0
  22. data/lib/clacky/ui2/components/common_component.rb +98 -0
  23. data/lib/clacky/ui2/components/inline_input.rb +187 -0
  24. data/lib/clacky/ui2/components/input_area.rb +1124 -0
  25. data/lib/clacky/ui2/components/message_component.rb +80 -0
  26. data/lib/clacky/ui2/components/output_area.rb +112 -0
  27. data/lib/clacky/ui2/components/todo_area.rb +130 -0
  28. data/lib/clacky/ui2/components/tool_component.rb +106 -0
  29. data/lib/clacky/ui2/components/welcome_banner.rb +103 -0
  30. data/lib/clacky/ui2/layout_manager.rb +437 -0
  31. data/lib/clacky/ui2/line_editor.rb +201 -0
  32. data/lib/clacky/ui2/markdown_renderer.rb +80 -0
  33. data/lib/clacky/ui2/screen_buffer.rb +257 -0
  34. data/lib/clacky/ui2/theme_manager.rb +68 -0
  35. data/lib/clacky/ui2/themes/base_theme.rb +85 -0
  36. data/lib/clacky/ui2/themes/hacker_theme.rb +58 -0
  37. data/lib/clacky/ui2/themes/minimal_theme.rb +52 -0
  38. data/lib/clacky/ui2/ui_controller.rb +778 -0
  39. data/lib/clacky/ui2/view_renderer.rb +177 -0
  40. data/lib/clacky/ui2.rb +37 -0
  41. data/lib/clacky/utils/file_ignore_helper.rb +126 -0
  42. data/lib/clacky/version.rb +1 -1
  43. data/lib/clacky.rb +1 -6
  44. metadata +53 -6
  45. data/lib/clacky/ui/banner.rb +0 -155
  46. data/lib/clacky/ui/enhanced_prompt.rb +0 -786
  47. data/lib/clacky/ui/formatter.rb +0 -209
  48. 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
- 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
@@ -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
- # Always run in interactive mode
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
- say "Loading latest session: #{session_data[:session_id][0..7]}", :green
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
- say "Loading session: #{session_data[:session_id][0..7]}", :green
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
- 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" }
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
- # Get the last N messages
622
- recent = conversation_messages.last(limit * 2) # *2 to get user+assistant pairs
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
- if recent.empty?
625
- return
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
- 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}"
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
- formatter.separator("─")
645
- say ""
646
- end
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
- def truncate_message(content, max_length)
649
- return "" if content.nil? || content.empty?
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
- # Remove excessive whitespace
652
- cleaned = content.strip.gsub(/\s+/, ' ')
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
- if cleaned.length > max_length
655
- cleaned[0...max_length] + "..."
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
- cleaned
412
+ ui_controller.initialize_and_show_banner
658
413
  end
659
- end
660
414
 
661
- # UI component accessors
662
- def ui_banner
663
- @ui_banner ||= UI::Banner.new
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
- def ui_prompt
667
- @ui_prompt ||= UI::EnhancedPrompt.new
668
- end
419
+ begin
420
+ # Set status to working when agent starts
421
+ ui_controller.set_working_status
669
422
 
670
- def ui_statusbar
671
- @ui_statusbar ||= UI::StatusBar.new
672
- end
423
+ result = agent.run(initial_message, images: [])
673
424
 
674
- def ui_formatter
675
- @ui_formatter ||= UI::Formatter.new
676
- end
425
+ if session_manager
426
+ session_manager.save(agent.to_session_data(status: :success))
427
+ end
677
428
 
678
- def pastel
679
- @pastel ||= Pastel.new
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