Class: Clacky::RichUIController
- Inherits:
-
Object
- Object
- Clacky::RichUIController
show all
- Includes:
- UIInterface
- Defined in:
- lib/clacky/rich_ui_controller.rb
Overview
Experimental RubyRich-backed TUI controller.
This intentionally implements the same surface as UI2::UIController so the CLI/Agent loop can switch implementations without knowing which TUI is underneath. It is not the default UI yet.
Defined Under Namespace
Classes: ConfigMenuDialog, FormDialog, LayoutAdapter, ProgressHandleAdapter
Constant Summary
collapse
- STREAMING_MARKDOWN_THRESHOLD =
240
- STREAMING_MARKDOWN_CHUNK_SIZE =
6
- STREAMING_MARKDOWN_DELAY =
0.03
- COMMANDS =
[
{ label: "/clear", value: "/clear", description: "Clear output and restart session" },
{ label: "/config", value: "/config", description: "Open configuration" },
{ label: "/undo", value: "/undo", description: "Restore a previous task state" },
{ label: "/help", value: "/help", description: "Show commands" },
{ label: "/exit", value: "/exit", description: "Exit application", aliases: ["/quit"] }
].freeze
Instance Attribute Summary collapse
Instance Method Summary
collapse
-
#append_output(content) ⇒ Object
-
#clear_input ⇒ Object
-
#filter_thinking_tags(content) ⇒ Object
-
#initialize(config = {}) ⇒ RichUIController
constructor
A new instance of RichUIController.
-
#initialize_and_show_banner(recent_user_messages: nil) ⇒ Object
-
#log(message, level: :info) ⇒ Object
-
#on_input(&block) ⇒ Object
-
#on_interrupt(&block) ⇒ Object
-
#on_mode_toggle(&block) ⇒ Object
-
#on_time_machine(&block) ⇒ Object
-
#request_confirmation(message, default: true) ⇒ Object
-
#set_agent(_agent, _agent_profile = nil) ⇒ Object
-
#set_idle_status ⇒ Object
-
#set_input_tips(message, type: :info) ⇒ Object
-
#set_skill_loader(_skill_loader, _agent_profile = nil) ⇒ Object
-
#set_working_status ⇒ Object
-
#show_assistant_message(content, files:) ⇒ Object
-
#show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil) ⇒ Object
-
#show_config_modal(current_config, test_callback: nil) ⇒ Object
-
#show_diff(old_content, new_content, max_lines: 50) ⇒ Object
-
#show_error(message) ⇒ Object
-
#show_file_edit_preview(path) ⇒ Object
-
#show_file_error(error_message) ⇒ Object
-
#show_file_write_preview(path, is_new_file:) ⇒ Object
-
#show_help ⇒ Object
-
#show_info(message, prefix_newline: true) ⇒ Object
-
#show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {}) ⇒ Object
-
#show_shell_preview(command) ⇒ Object
-
#show_success(message) ⇒ Object
-
#show_token_usage(token_data) ⇒ Object
-
#show_tool_args(formatted_args) ⇒ Object
-
#show_tool_call(name, args) ⇒ Object
-
#show_tool_error(error) ⇒ Object
-
#show_tool_result(result) ⇒ Object
-
#show_tool_stdout(lines) ⇒ Object
-
#show_warning(message) ⇒ Object
-
#start ⇒ Object
-
#start_input_loop ⇒ Object
-
#start_progress(message: nil, style: :primary, quiet_on_fast_finish: false) ⇒ Object
-
#stop(clear_screen: false) ⇒ Object
-
#update_sessionbar(tasks: nil, cost: nil, cost_source: nil, status: nil, latency: nil, session_id: nil) ⇒ Object
-
#update_todos(todos) ⇒ Object
-
#with_progress(message: nil, style: :primary, quiet_on_fast_finish: false) ⇒ Object
#show_feedback_request, #stream_thinking_progress
Constructor Details
Returns a new instance of RichUIController.
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
|
# File 'lib/clacky/rich_ui_controller.rb', line 504
def initialize(config = {})
@config = {
working_dir: config[:working_dir],
mode: config[:mode],
model: config[:model],
theme: config[:theme]
}
@welcome_banner = Clacky::UI2::Components::WelcomeBanner.new
@shell = RichAgentShell.new(
title: "OpenClacky",
subtitle: config[:working_dir].to_s,
model: config[:model].to_s,
commands: COMMANDS
)
@layout = LayoutAdapter.new(@shell)
@input_callback = nil
@interrupt_callback = nil
@mode_toggle_callback = nil
@time_machine_callback = nil
@tasks_count = 0
@total_cost = 0.0
@running = false
@tool_ids = []
@todo_items = []
@explicit_todo_cycle = false
@tool_activities = []
@tool_activity_by_id = {}
@legacy_progress = {}
@stdout_lines = []
@callback_threads = []
@stream_threads = []
wire_shell_callbacks
end
|
Instance Attribute Details
#config ⇒ Object
Returns the value of attribute config.
502
503
504
|
# File 'lib/clacky/rich_ui_controller.rb', line 502
def config
@config
end
|
#layout ⇒ Object
Returns the value of attribute layout.
501
502
503
|
# File 'lib/clacky/rich_ui_controller.rb', line 501
def layout
@layout
end
|
#running ⇒ Object
Returns the value of attribute running.
501
502
503
|
# File 'lib/clacky/rich_ui_controller.rb', line 501
def running
@running
end
|
#shell ⇒ Object
Returns the value of attribute shell.
501
502
503
|
# File 'lib/clacky/rich_ui_controller.rb', line 501
def shell
@shell
end
|
Instance Method Details
#append_output(content) ⇒ Object
587
588
589
590
591
|
# File 'lib/clacky/rich_ui_controller.rb', line 587
def append_output(content)
return if content.nil?
@shell.add_markdown(content.to_s)
end
|
774
775
776
|
# File 'lib/clacky/rich_ui_controller.rb', line 774
def clear_input
@shell.composer.editor.clear
end
|
847
848
849
850
851
|
# File 'lib/clacky/rich_ui_controller.rb', line 847
def filter_thinking_tags(content)
return content if content.nil?
content.gsub(%r{<think(?:ing)?>[\s\S]*?</think(?:ing)?>}mi, "").gsub(/\n{3,}/, "\n\n").strip
end
|
#initialize_and_show_banner(recent_user_messages: nil) ⇒ Object
539
540
541
542
543
544
545
546
547
548
|
# File 'lib/clacky/rich_ui_controller.rb', line 539
def initialize_and_show_banner(recent_user_messages: nil)
@running = true
@shell.update_status(session_status)
if recent_user_messages && !recent_user_messages.empty?
@shell.add_separator("recent session")
recent_user_messages.each { |message| @shell.add_user_message(message) }
else
add_plain_block(render_welcome_banner)
end
end
|
#log(message, level: :info) ⇒ Object
593
594
595
596
597
598
599
600
|
# File 'lib/clacky/rich_ui_controller.rb', line 593
def log(message, level: :info)
case level.to_sym
when :error then show_error(message)
when :warning, :warn then show_warning(message)
when :debug then nil
else show_info(message)
end
end
|
571
572
573
|
# File 'lib/clacky/rich_ui_controller.rb', line 571
def on_input(&block)
@input_callback = block
end
|
#on_interrupt(&block) ⇒ Object
575
576
577
|
# File 'lib/clacky/rich_ui_controller.rb', line 575
def on_interrupt(&block)
@interrupt_callback = block
end
|
#on_mode_toggle(&block) ⇒ Object
579
580
581
|
# File 'lib/clacky/rich_ui_controller.rb', line 579
def on_mode_toggle(&block)
@mode_toggle_callback = block
end
|
#on_time_machine(&block) ⇒ Object
583
584
585
|
# File 'lib/clacky/rich_ui_controller.rb', line 583
def on_time_machine(&block)
@time_machine_callback = block
end
|
#request_confirmation(message, default: true) ⇒ Object
764
765
766
767
768
769
770
771
772
|
# File 'lib/clacky/rich_ui_controller.rb', line 764
def request_confirmation(message, default: true)
show_info(message)
@shell.confirm(
title: "Confirm",
message: message,
choices: [{ key: true, label: "Yes" }, { key: false, label: "No" }],
default: default
)
end
|
#set_agent(_agent, _agent_profile = nil) ⇒ Object
569
|
# File 'lib/clacky/rich_ui_controller.rb', line 569
def set_agent(_agent, _agent_profile = nil); end
|
#set_idle_status ⇒ Object
760
761
762
|
# File 'lib/clacky/rich_ui_controller.rb', line 760
def set_idle_status
update_sessionbar(status: "idle")
end
|
778
779
780
|
# File 'lib/clacky/rich_ui_controller.rb', line 778
def set_input_tips(message, type: :info)
update_sessionbar(status: "#{type}: #{message}")
end
|
#set_skill_loader(_skill_loader, _agent_profile = nil) ⇒ Object
568
|
# File 'lib/clacky/rich_ui_controller.rb', line 568
def set_skill_loader(_skill_loader, _agent_profile = nil); end
|
#set_working_status ⇒ Object
756
757
758
|
# File 'lib/clacky/rich_ui_controller.rb', line 756
def set_working_status
update_sessionbar(status: "working")
end
|
#show_assistant_message(content, files:) ⇒ Object
602
603
604
605
606
607
608
609
610
611
|
# File 'lib/clacky/rich_ui_controller.rb', line 602
def show_assistant_message(content, files:)
text = filter_thinking_tags(content)
stream_thread = nil
stream_thread = add_conversation_markdown(text) unless text.nil? || text.strip.empty?
if stream_thread.is_a?(Thread)
add_file_summary_after(stream_thread, files)
else
add_file_summary(files)
end
end
|
#show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil) ⇒ Object
685
686
687
688
689
690
691
692
|
# File 'lib/clacky/rich_ui_controller.rb', line 685
def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil)
set_idle_status
return if awaiting_user_feedback || iterations <= 5
parts = ["Completed #{iterations} iterations", "cost $#{cost.round(4)}"]
parts << "#{duration.round(1)}s" if duration
append_output(parts.join(" ยท "))
end
|
#show_config_modal(current_config, test_callback: nil) ⇒ Object
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
|
# File 'lib/clacky/rich_ui_controller.rb', line 795
def show_config_modal(current_config, test_callback: nil)
return nil unless @running
loop do
choices = (current_config)
result = (
title: "Model Configuration",
choices: choices,
selected_index: config_initial_selection(choices)
)
return nil if result.nil?
case result[:action]
when :switch
return result
when :add
new_model = show_model_edit_form(nil, test_callback: test_callback)
if new_model
anthropic_format = new_model[:provider] == "anthropic"
current_config.add_model(
model: new_model[:model],
api_key: new_model[:api_key],
base_url: new_model[:base_url],
anthropic_format: anthropic_format
)
new_id = current_config.models.last["id"]
return { action: :add, model_id: new_id }
end
when :edit
current_model = current_config.current_model
edited = show_model_edit_form(current_model, test_callback: test_callback)
if edited
current_model["api_key"] = edited[:api_key]
current_model["model"] = edited[:model]
current_model["base_url"] = edited[:base_url]
return { action: :edit, model_id: current_model["id"] }
end
when :delete
if current_config.models.length <= 1
show_warning("Cannot delete the last model.")
next
end
current_config.remove_model(current_config.current_model_index)
new_current = current_config.current_model
return { action: :delete, model_id: new_current && new_current["id"] }
when :close
return nil
end
end
end
|
#show_diff(old_content, new_content, max_lines: 50) ⇒ Object
664
665
666
667
668
669
670
671
672
673
674
|
# File 'lib/clacky/rich_ui_controller.rb', line 664
def show_diff(old_content, new_content, max_lines: 50)
require "diffy"
diff = Diffy::Diff.new(old_content, new_content, context: 3).to_s(:color)
lines = diff.lines
visible = lines.take(max_lines).join
hidden = lines.length - max_lines
visible += "\n... (#{hidden} more lines hidden)" if hidden.positive?
@shell.add_diff(content: visible)
rescue LoadError
append_output("Old size: #{old_content.bytesize} bytes\nNew size: #{new_content.bytesize} bytes")
end
|
#show_error(message) ⇒ Object
703
704
705
|
# File 'lib/clacky/rich_ui_controller.rb', line 703
def show_error(message)
@shell.add_error_message(message.to_s)
end
|
#show_file_edit_preview(path) ⇒ Object
652
653
654
|
# File 'lib/clacky/rich_ui_controller.rb', line 652
def show_file_edit_preview(path)
append_output("Editing file: #{path || "(unknown)"}")
end
|
#show_file_error(error_message) ⇒ Object
656
657
658
|
# File 'lib/clacky/rich_ui_controller.rb', line 656
def show_file_error(error_message)
show_error(error_message)
end
|
#show_file_write_preview(path, is_new_file:) ⇒ Object
648
649
650
|
# File 'lib/clacky/rich_ui_controller.rb', line 648
def show_file_write_preview(path, is_new_file:)
append_output("#{is_new_file ? "Creating" : "Modifying"} file: #{path || "(unknown)"}")
end
|
#show_help ⇒ Object
782
783
784
785
786
787
788
789
790
791
792
793
|
# File 'lib/clacky/rich_ui_controller.rb', line 782
def show_help
@shell.add_markdown(<<~HELP)
Commands:
/clear - Clear output and restart session
/exit - Exit application
Input:
Shift+Enter - New line
Up/Down - History navigation
Ctrl+C - Interrupt current task
HELP
end
|
#show_info(message, prefix_newline: true) ⇒ Object
694
695
696
697
|
# File 'lib/clacky/rich_ui_controller.rb', line 694
def show_info(message, prefix_newline: true)
_ = prefix_newline
@shell.add_system_message(message.to_s)
end
|
#show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {}) ⇒ Object
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
|
# File 'lib/clacky/rich_ui_controller.rb', line 711
def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {})
_ = prefix_newline
type = progress_type.to_s
if phase.to_s == "done"
@legacy_progress.delete(type)&.finish(final_message: message)
return
end
handle = @legacy_progress[type]
if handle
handle.update(message: message, metadata: metadata)
else
@legacy_progress[type] = start_progress(message: message, style: type == "thinking" ? :primary : :quiet)
end
end
|
#show_shell_preview(command) ⇒ Object
660
661
662
|
# File 'lib/clacky/rich_ui_controller.rb', line 660
def show_shell_preview(command)
append_output("$ #{command}")
end
|
#show_success(message) ⇒ Object
707
708
709
|
# File 'lib/clacky/rich_ui_controller.rb', line 707
def show_success(message)
@shell.add_system_message("OK: #{message}")
end
|
#show_token_usage(token_data) ⇒ Object
676
677
678
679
680
681
682
683
|
# File 'lib/clacky/rich_ui_controller.rb', line 676
def show_token_usage(token_data)
@shell.show_token_usage(
input: token_data[:prompt_tokens],
output: token_data[:completion_tokens],
total: token_data[:total_tokens],
cost: token_data[:cost]
)
end
|
644
645
646
|
# File 'lib/clacky/rich_ui_controller.rb', line 644
def show_tool_args(formatted_args)
append_output("Args: #{formatted_args}")
end
|
613
614
615
616
617
618
619
|
# File 'lib/clacky/rich_ui_controller.rb', line 613
def show_tool_call(name, args)
id = @shell.start_tool_call(name: name.to_s, input: format_args(args), status: :running)
if id
@tool_ids << id
track_tool_activity(id, tool_activity_label(name, args), :running)
end
end
|
634
635
636
637
638
639
640
641
642
|
# File 'lib/clacky/rich_ui_controller.rb', line 634
def show_tool_error(error)
message = error.is_a?(Exception) ? error.message : error.to_s
if (id = @tool_ids.pop)
@shell.finish_tool_call(id, status: :error, output: message)
update_tool_activity(id, :error)
else
@shell.add_error_message(message)
end
end
|
621
622
623
624
625
626
627
628
|
# File 'lib/clacky/rich_ui_controller.rb', line 621
def show_tool_result(result)
if (id = @tool_ids.pop)
@shell.finish_tool_call(id, status: :done, output: result.to_s)
update_tool_activity(id, :done)
else
@shell.add_markdown(result.to_s)
end
end
|
630
631
632
|
# File 'lib/clacky/rich_ui_controller.rb', line 630
def show_tool_stdout(lines)
@stdout_lines.concat(Array(lines).map(&:to_s))
end
|
#show_warning(message) ⇒ Object
699
700
701
|
# File 'lib/clacky/rich_ui_controller.rb', line 699
def show_warning(message)
@shell.add_system_message("Warning: #{message}")
end
|
#start ⇒ Object
550
551
552
553
|
# File 'lib/clacky/rich_ui_controller.rb', line 550
def start
initialize_and_show_banner unless @running
start_input_loop
end
|
555
556
557
558
559
560
|
# File 'lib/clacky/rich_ui_controller.rb', line 555
def start_input_loop
@running = true
@shell.start
ensure
@running = false
end
|
#start_progress(message: nil, style: :primary, quiet_on_fast_finish: false) ⇒ Object
727
728
729
730
|
# File 'lib/clacky/rich_ui_controller.rb', line 727
def start_progress(message: nil, style: :primary, quiet_on_fast_finish: false)
_ = quiet_on_fast_finish
ProgressHandleAdapter.new(@shell.start_progress(message || "Working", style: style))
end
|
#stop(clear_screen: false) ⇒ Object
562
563
564
565
566
|
# File 'lib/clacky/rich_ui_controller.rb', line 562
def stop(clear_screen: false)
@running = false
@shell.stop
RubyRich::Terminal.clear if clear_screen
end
|
#update_sessionbar(tasks: nil, cost: nil, cost_source: nil, status: nil, latency: nil, session_id: nil) ⇒ Object
741
742
743
744
745
746
747
748
|
# File 'lib/clacky/rich_ui_controller.rb', line 741
def update_sessionbar(tasks: nil, cost: nil, cost_source: nil, status: nil, latency: nil, session_id: nil)
_ = cost_source
_ = latency
@tasks_count = tasks if tasks
@total_cost = cost if cost
@status = status if status
@shell.update_status(session_status)
end
|
#update_todos(todos) ⇒ Object
750
751
752
753
754
|
# File 'lib/clacky/rich_ui_controller.rb', line 750
def update_todos(todos)
@todo_items = Array(todos).map { |todo| normalize_todo(todo) }
@explicit_todo_cycle = true
end
|
#with_progress(message: nil, style: :primary, quiet_on_fast_finish: false) ⇒ Object
732
733
734
735
736
737
738
739
|
# File 'lib/clacky/rich_ui_controller.rb', line 732
def with_progress(message: nil, style: :primary, quiet_on_fast_finish: false)
handle = start_progress(message: message, style: style, quiet_on_fast_finish: quiet_on_fast_finish)
begin
yield handle
ensure
handle.finish
end
end
|