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: true) ⇒ Object
Clears the screen on exit by default — the Rich UI repaints fullscreen and leaves no useful scrollback to preserve.
-
#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
#phase_end, #phase_start, #show_feedback_request, #stream_thinking_progress, #with_phase
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
589
590
591
592
593
|
# File 'lib/clacky/rich_ui_controller.rb', line 589
def append_output(content)
return if content.nil?
@shell.add_markdown(content.to_s)
end
|
776
777
778
|
# File 'lib/clacky/rich_ui_controller.rb', line 776
def clear_input
@shell.composer.editor.clear
end
|
849
850
851
852
853
|
# File 'lib/clacky/rich_ui_controller.rb', line 849
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
595
596
597
598
599
600
601
602
|
# File 'lib/clacky/rich_ui_controller.rb', line 595
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
|
573
574
575
|
# File 'lib/clacky/rich_ui_controller.rb', line 573
def on_input(&block)
@input_callback = block
end
|
#on_interrupt(&block) ⇒ Object
577
578
579
|
# File 'lib/clacky/rich_ui_controller.rb', line 577
def on_interrupt(&block)
@interrupt_callback = block
end
|
#on_mode_toggle(&block) ⇒ Object
581
582
583
|
# File 'lib/clacky/rich_ui_controller.rb', line 581
def on_mode_toggle(&block)
@mode_toggle_callback = block
end
|
#on_time_machine(&block) ⇒ Object
585
586
587
|
# File 'lib/clacky/rich_ui_controller.rb', line 585
def on_time_machine(&block)
@time_machine_callback = block
end
|
#request_confirmation(message, default: true) ⇒ Object
766
767
768
769
770
771
772
773
774
|
# File 'lib/clacky/rich_ui_controller.rb', line 766
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
571
|
# File 'lib/clacky/rich_ui_controller.rb', line 571
def set_agent(_agent, _agent_profile = nil); end
|
#set_idle_status ⇒ Object
762
763
764
|
# File 'lib/clacky/rich_ui_controller.rb', line 762
def set_idle_status
update_sessionbar(status: "idle")
end
|
780
781
782
|
# File 'lib/clacky/rich_ui_controller.rb', line 780
def set_input_tips(message, type: :info)
update_sessionbar(status: "#{type}: #{message}")
end
|
#set_skill_loader(_skill_loader, _agent_profile = nil) ⇒ Object
570
|
# File 'lib/clacky/rich_ui_controller.rb', line 570
def set_skill_loader(_skill_loader, _agent_profile = nil); end
|
#set_working_status ⇒ Object
758
759
760
|
# File 'lib/clacky/rich_ui_controller.rb', line 758
def set_working_status
update_sessionbar(status: "working")
end
|
#show_assistant_message(content, files:) ⇒ Object
604
605
606
607
608
609
610
611
612
613
|
# File 'lib/clacky/rich_ui_controller.rb', line 604
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
687
688
689
690
691
692
693
694
|
# File 'lib/clacky/rich_ui_controller.rb', line 687
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
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
846
847
|
# File 'lib/clacky/rich_ui_controller.rb', line 797
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
666
667
668
669
670
671
672
673
674
675
676
|
# File 'lib/clacky/rich_ui_controller.rb', line 666
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
705
706
707
|
# File 'lib/clacky/rich_ui_controller.rb', line 705
def show_error(message)
@shell.add_error_message(message.to_s)
end
|
#show_file_edit_preview(path) ⇒ Object
654
655
656
|
# File 'lib/clacky/rich_ui_controller.rb', line 654
def show_file_edit_preview(path)
append_output("Editing file: #{path || "(unknown)"}")
end
|
#show_file_error(error_message) ⇒ Object
658
659
660
|
# File 'lib/clacky/rich_ui_controller.rb', line 658
def show_file_error(error_message)
show_error(error_message)
end
|
#show_file_write_preview(path, is_new_file:) ⇒ Object
650
651
652
|
# File 'lib/clacky/rich_ui_controller.rb', line 650
def show_file_write_preview(path, is_new_file:)
append_output("#{is_new_file ? "Creating" : "Modifying"} file: #{path || "(unknown)"}")
end
|
#show_help ⇒ Object
784
785
786
787
788
789
790
791
792
793
794
795
|
# File 'lib/clacky/rich_ui_controller.rb', line 784
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
696
697
698
699
|
# File 'lib/clacky/rich_ui_controller.rb', line 696
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
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
|
# File 'lib/clacky/rich_ui_controller.rb', line 713
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
662
663
664
|
# File 'lib/clacky/rich_ui_controller.rb', line 662
def show_shell_preview(command)
append_output("$ #{command}")
end
|
#show_success(message) ⇒ Object
709
710
711
|
# File 'lib/clacky/rich_ui_controller.rb', line 709
def show_success(message)
@shell.add_system_message("OK: #{message}")
end
|
#show_token_usage(token_data) ⇒ Object
678
679
680
681
682
683
684
685
|
# File 'lib/clacky/rich_ui_controller.rb', line 678
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
|
646
647
648
|
# File 'lib/clacky/rich_ui_controller.rb', line 646
def show_tool_args(formatted_args)
append_output("Args: #{formatted_args}")
end
|
615
616
617
618
619
620
621
|
# File 'lib/clacky/rich_ui_controller.rb', line 615
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
|
636
637
638
639
640
641
642
643
644
|
# File 'lib/clacky/rich_ui_controller.rb', line 636
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
|
623
624
625
626
627
628
629
630
|
# File 'lib/clacky/rich_ui_controller.rb', line 623
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
|
632
633
634
|
# File 'lib/clacky/rich_ui_controller.rb', line 632
def show_tool_stdout(lines)
@stdout_lines.concat(Array(lines).map(&:to_s))
end
|
#show_warning(message) ⇒ Object
701
702
703
|
# File 'lib/clacky/rich_ui_controller.rb', line 701
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
729
730
731
732
|
# File 'lib/clacky/rich_ui_controller.rb', line 729
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: true) ⇒ Object
Clears the screen on exit by default — the Rich UI repaints fullscreen and leaves no useful scrollback to preserve.
564
565
566
567
568
|
# File 'lib/clacky/rich_ui_controller.rb', line 564
def stop(clear_screen: true)
@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
743
744
745
746
747
748
749
750
|
# File 'lib/clacky/rich_ui_controller.rb', line 743
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
752
753
754
755
756
|
# File 'lib/clacky/rich_ui_controller.rb', line 752
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
734
735
736
737
738
739
740
741
|
# File 'lib/clacky/rich_ui_controller.rb', line 734
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
|