openclacky 0.7.0 → 0.7.2
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/.clacky/skills/commit/SKILL.md +29 -4
- data/.clackyrules +3 -1
- data/CHANGELOG.md +103 -2
- data/README.md +70 -161
- data/bin/clarky +11 -0
- data/docs/HOW-TO-USE-CN.md +96 -0
- data/docs/HOW-TO-USE.md +94 -0
- data/docs/config.example.yml +27 -0
- data/docs/deploy_subagent_design.md +540 -0
- data/docs/time_machine_design.md +247 -0
- data/docs/why-openclacky.md +0 -1
- data/lib/clacky/agent/cost_tracker.rb +180 -0
- data/lib/clacky/agent/llm_caller.rb +54 -0
- data/lib/clacky/{message_compressor.rb → agent/message_compressor.rb} +12 -36
- data/lib/clacky/agent/message_compressor_helper.rb +534 -0
- data/lib/clacky/agent/session_serializer.rb +152 -0
- data/lib/clacky/agent/skill_manager.rb +138 -0
- data/lib/clacky/agent/system_prompt_builder.rb +96 -0
- data/lib/clacky/agent/time_machine.rb +199 -0
- data/lib/clacky/agent/tool_executor.rb +434 -0
- data/lib/clacky/{tool_registry.rb → agent/tool_registry.rb} +1 -1
- data/lib/clacky/agent.rb +260 -1370
- data/lib/clacky/agent_config.rb +447 -10
- data/lib/clacky/cli.rb +275 -98
- data/lib/clacky/client.rb +12 -2
- data/lib/clacky/default_skills/code-explorer/SKILL.md +34 -0
- data/lib/clacky/default_skills/deploy/SKILL.md +13 -0
- data/lib/clacky/default_skills/deploy/scripts/rails_deploy.rb +383 -0
- data/lib/clacky/default_skills/deploy/tools/check_health.rb +116 -0
- data/lib/clacky/default_skills/deploy/tools/execute_deployment.rb +174 -0
- data/lib/clacky/default_skills/deploy/tools/fetch_runtime_logs.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/list_services.rb +80 -0
- data/lib/clacky/default_skills/deploy/tools/report_deploy_status.rb +67 -0
- data/lib/clacky/default_skills/deploy/tools/set_deploy_variables.rb +138 -0
- data/lib/clacky/default_skills/new/SKILL.md +2 -2
- data/lib/clacky/json_ui_controller.rb +195 -0
- data/lib/clacky/providers.rb +107 -0
- data/lib/clacky/skill.rb +48 -7
- data/lib/clacky/skill_loader.rb +7 -0
- data/lib/clacky/tools/edit.rb +105 -48
- data/lib/clacky/tools/file_reader.rb +44 -73
- data/lib/clacky/tools/invoke_skill.rb +89 -0
- data/lib/clacky/tools/list_tasks.rb +54 -0
- data/lib/clacky/tools/redo_task.rb +41 -0
- data/lib/clacky/tools/safe_shell.rb +1 -1
- data/lib/clacky/tools/shell.rb +74 -62
- data/lib/clacky/tools/trash_manager.rb +1 -1
- data/lib/clacky/tools/undo_task.rb +32 -0
- data/lib/clacky/tools/web_fetch.rb +2 -1
- data/lib/clacky/ui2/components/command_suggestions.rb +13 -3
- data/lib/clacky/ui2/components/inline_input.rb +23 -2
- data/lib/clacky/ui2/components/input_area.rb +65 -21
- data/lib/clacky/ui2/components/modal_component.rb +199 -62
- data/lib/clacky/ui2/layout_manager.rb +75 -25
- data/lib/clacky/ui2/line_editor.rb +23 -2
- data/lib/clacky/ui2/markdown_renderer.rb +31 -10
- data/lib/clacky/ui2/screen_buffer.rb +2 -0
- data/lib/clacky/ui2/ui_controller.rb +316 -37
- data/lib/clacky/ui2.rb +2 -0
- data/lib/clacky/ui_interface.rb +50 -0
- data/lib/clacky/utils/arguments_parser.rb +31 -3
- data/lib/clacky/utils/file_processor.rb +13 -18
- data/lib/clacky/version.rb +1 -1
- data/lib/clacky.rb +19 -9
- data/scripts/install.sh +274 -97
- data/scripts/uninstall.sh +12 -12
- metadata +40 -13
- data/.clacky/skills/test-skill/SKILL.md +0 -15
- data/lib/clacky/compression/base.rb +0 -231
- data/lib/clacky/compression/standard.rb +0 -339
- data/lib/clacky/config.rb +0 -117
- /data/lib/clacky/{hook_manager.rb → agent/hook_manager.rb} +0 -0
- /data/lib/clacky/{progress_indicator.rb → ui2/progress_indicator.rb} +0 -0
- /data/lib/clacky/{thinking_verbs.rb → ui2/thinking_verbs.rb} +0 -0
- /data/lib/clacky/{gitignore_parser.rb → utils/gitignore_parser.rb} +0 -0
- /data/lib/clacky/{model_pricing.rb → utils/model_pricing.rb} +0 -0
- /data/lib/clacky/{trash_directory.rb → utils/trash_directory.rb} +0 -0
|
@@ -6,12 +6,15 @@ require_relative "components/input_area"
|
|
|
6
6
|
require_relative "components/todo_area"
|
|
7
7
|
require_relative "components/welcome_banner"
|
|
8
8
|
require_relative "components/inline_input"
|
|
9
|
-
require_relative "
|
|
9
|
+
require_relative "thinking_verbs"
|
|
10
|
+
require_relative "../ui_interface"
|
|
10
11
|
|
|
11
12
|
module Clacky
|
|
12
13
|
module UI2
|
|
13
14
|
# UIController is the MVC controller layer that coordinates UI state and user interactions
|
|
14
15
|
class UIController
|
|
16
|
+
include Clacky::UIInterface
|
|
17
|
+
|
|
15
18
|
attr_reader :layout, :renderer, :running, :inline_input, :input_area
|
|
16
19
|
attr_accessor :config
|
|
17
20
|
|
|
@@ -42,11 +45,13 @@ module Clacky
|
|
|
42
45
|
@running = false
|
|
43
46
|
@input_callback = nil
|
|
44
47
|
@interrupt_callback = nil
|
|
48
|
+
@time_machine_callback = nil
|
|
45
49
|
@tasks_count = 0
|
|
46
50
|
@total_cost = 0.0
|
|
47
51
|
@progress_thread = nil
|
|
48
52
|
@progress_start_time = nil
|
|
49
53
|
@progress_message = nil
|
|
54
|
+
@last_diff_lines = nil
|
|
50
55
|
end
|
|
51
56
|
|
|
52
57
|
# Start the UI controller
|
|
@@ -129,6 +134,18 @@ module Clacky
|
|
|
129
134
|
@layout.cleanup_screen
|
|
130
135
|
end
|
|
131
136
|
|
|
137
|
+
# Clear the input area
|
|
138
|
+
def clear_input
|
|
139
|
+
@input_area.clear
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Set input tips message
|
|
143
|
+
# @param message [String] Tip message to display
|
|
144
|
+
# @param type [Symbol] Tip type (:info, :warning, etc.)
|
|
145
|
+
def set_input_tips(message, type: :info)
|
|
146
|
+
@input_area.set_tips(message, type: type)
|
|
147
|
+
end
|
|
148
|
+
|
|
132
149
|
# Set callback for user input
|
|
133
150
|
# @param block [Proc] Callback to execute with user input
|
|
134
151
|
def on_input(&block)
|
|
@@ -147,6 +164,12 @@ module Clacky
|
|
|
147
164
|
@mode_toggle_callback = block
|
|
148
165
|
end
|
|
149
166
|
|
|
167
|
+
# Set callback for time machine (ESC key)
|
|
168
|
+
# @param block [Proc] Callback to execute on time machine
|
|
169
|
+
def on_time_machine(&block)
|
|
170
|
+
@time_machine_callback = block
|
|
171
|
+
end
|
|
172
|
+
|
|
150
173
|
# Set skill loader for command suggestions
|
|
151
174
|
# @param skill_loader [Clacky::SkillLoader] The skill loader instance
|
|
152
175
|
def set_skill_loader(skill_loader)
|
|
@@ -621,18 +644,31 @@ module Clacky
|
|
|
621
644
|
diff = Diffy::Diff.new(old_content, new_content, context: 3)
|
|
622
645
|
diff_lines = diff.to_s(:color).lines
|
|
623
646
|
|
|
647
|
+
# Store for fullscreen toggle
|
|
648
|
+
@last_diff_lines = diff_lines
|
|
649
|
+
|
|
624
650
|
# Show diff without line numbers
|
|
625
651
|
diff_lines.take(max_lines).each do |line|
|
|
626
652
|
append_output(line.chomp)
|
|
627
653
|
end
|
|
628
654
|
|
|
629
655
|
if diff_lines.size > max_lines
|
|
630
|
-
append_output("\n... (#{diff_lines.size - max_lines} more lines, diff truncated)")
|
|
656
|
+
append_output("\n... (#{diff_lines.size - max_lines} more lines, diff truncated. Press Ctrl+O to expand)")
|
|
631
657
|
end
|
|
632
658
|
rescue LoadError
|
|
633
659
|
# Fallback if diffy is not available
|
|
634
660
|
append_output(" Old size: #{old_content.bytesize} bytes")
|
|
635
661
|
append_output(" New size: #{new_content.bytesize} bytes")
|
|
662
|
+
@last_diff_lines = nil
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
# Show fullscreen diff view (only if not already expanded)
|
|
666
|
+
private def redisplay_diff
|
|
667
|
+
return unless @last_diff_lines
|
|
668
|
+
return if @layout.fullscreen_mode? # Already in fullscreen, ignore
|
|
669
|
+
|
|
670
|
+
# Enter fullscreen diff mode
|
|
671
|
+
@layout.enter_fullscreen(@last_diff_lines, hint: "Press Ctrl+O to return")
|
|
636
672
|
end
|
|
637
673
|
|
|
638
674
|
private
|
|
@@ -686,15 +722,17 @@ module Clacky
|
|
|
686
722
|
)
|
|
687
723
|
append_output(content)
|
|
688
724
|
|
|
689
|
-
# Check if API key is configured
|
|
725
|
+
# Check if API key is configured (show warning AFTER banner)
|
|
690
726
|
check_api_key_configuration
|
|
691
727
|
end
|
|
692
728
|
|
|
693
729
|
# Check if API key is configured and show warning if missing
|
|
694
730
|
private def check_api_key_configuration
|
|
695
|
-
config = Clacky::
|
|
731
|
+
config = Clacky::AgentConfig.load
|
|
696
732
|
|
|
697
|
-
if config.
|
|
733
|
+
if !config.models_configured?
|
|
734
|
+
show_warning("No models configured! Please run /config to set up your models and API keys.")
|
|
735
|
+
elsif config.api_key.nil? || config.api_key.empty?
|
|
698
736
|
show_warning("API key is not configured! Please run /config to set up your API key.")
|
|
699
737
|
end
|
|
700
738
|
end
|
|
@@ -748,6 +786,14 @@ module Clacky
|
|
|
748
786
|
# Handle keyboard input - delegate to InputArea or InlineInput
|
|
749
787
|
# @param key [Symbol, String, Hash] Key input or rapid input hash
|
|
750
788
|
def handle_key(key)
|
|
789
|
+
# If in fullscreen mode, only handle Ctrl+O to exit
|
|
790
|
+
if @layout.fullscreen_mode?
|
|
791
|
+
if key == :ctrl_o
|
|
792
|
+
@layout.exit_fullscreen
|
|
793
|
+
end
|
|
794
|
+
return
|
|
795
|
+
end
|
|
796
|
+
|
|
751
797
|
# If InlineInput is active, delegate to it
|
|
752
798
|
if @inline_input&.active?
|
|
753
799
|
handle_inline_input_key(key)
|
|
@@ -778,19 +824,23 @@ module Clacky
|
|
|
778
824
|
# Notify CLI to handle interrupt (stop agent or exit)
|
|
779
825
|
@interrupt_callback&.call(input_was_empty: input_was_empty)
|
|
780
826
|
when :clear_output
|
|
781
|
-
#
|
|
782
|
-
@
|
|
783
|
-
# Notify the callback to reset session/agent
|
|
784
|
-
@input_callback&.call("/clear", [])
|
|
827
|
+
# Pass to callback with data for display
|
|
828
|
+
@input_callback&.call("/clear", [], display: result[:data][:display])
|
|
785
829
|
when :scroll_up
|
|
786
830
|
@layout.scroll_output_up
|
|
787
831
|
when :scroll_down
|
|
788
832
|
@layout.scroll_output_down
|
|
789
833
|
when :help
|
|
790
|
-
|
|
791
|
-
@
|
|
834
|
+
# Pass to callback with data for display
|
|
835
|
+
@input_callback&.call("/help", [], display: result[:data][:display])
|
|
792
836
|
when :toggle_mode
|
|
793
837
|
toggle_mode
|
|
838
|
+
when :toggle_expand
|
|
839
|
+
# Enter fullscreen diff view
|
|
840
|
+
redisplay_diff
|
|
841
|
+
when :time_machine
|
|
842
|
+
# Trigger time machine callback
|
|
843
|
+
@time_machine_callback&.call
|
|
794
844
|
end
|
|
795
845
|
|
|
796
846
|
# Always re-render input area after key handling
|
|
@@ -813,6 +863,9 @@ module Clacky
|
|
|
813
863
|
when :submit, :cancel
|
|
814
864
|
# InlineInput is done, will be cleaned up by request_confirmation after collect returns
|
|
815
865
|
nil
|
|
866
|
+
when :toggle_expand
|
|
867
|
+
# Enter fullscreen diff view (will return when user presses Ctrl+O)
|
|
868
|
+
redisplay_diff
|
|
816
869
|
when :toggle_mode
|
|
817
870
|
# Update mode and session bar info, but don't render yet
|
|
818
871
|
current_mode = @config[:mode]
|
|
@@ -841,64 +894,290 @@ module Clacky
|
|
|
841
894
|
|
|
842
895
|
# Handle submit action
|
|
843
896
|
private def handle_submit(data)
|
|
844
|
-
#
|
|
845
|
-
@input_callback&.call(data[:text], data[:images])
|
|
846
|
-
|
|
847
|
-
# Append the input content to output area after callback completes
|
|
897
|
+
# Append the input content to output area first (so it displays before callback execution)
|
|
848
898
|
@layout.append_output(data[:display]) unless data[:display].empty?
|
|
899
|
+
|
|
900
|
+
# Then call callback (allows interrupting previous agent before processing new input)
|
|
901
|
+
@input_callback&.call(data[:text], data[:images])
|
|
849
902
|
end
|
|
850
903
|
|
|
851
|
-
# Show configuration modal dialog
|
|
852
|
-
# @param current_config [Clacky::
|
|
904
|
+
# Show configuration modal dialog with multi-model support
|
|
905
|
+
# @param current_config [Clacky::AgentConfig] Current configuration object
|
|
853
906
|
# @return [Hash, nil] Hash with updated config values, or nil if cancelled
|
|
854
907
|
public def show_config_modal(current_config, test_callback: nil)
|
|
855
908
|
modal = Components::ModalComponent.new
|
|
909
|
+
|
|
910
|
+
loop do
|
|
911
|
+
# Build menu choices
|
|
912
|
+
choices = []
|
|
913
|
+
|
|
914
|
+
# Add model list
|
|
915
|
+
current_config.models.each_with_index do |model, idx|
|
|
916
|
+
is_current = (idx == current_config.current_model_index)
|
|
917
|
+
model_name = model["model"] || "unnamed"
|
|
918
|
+
masked_key = mask_api_key(model["api_key"])
|
|
919
|
+
|
|
920
|
+
# Add type badge if present
|
|
921
|
+
type_badge = case model["type"]
|
|
922
|
+
when "default" then "[default] "
|
|
923
|
+
when "lite" then "[lite] "
|
|
924
|
+
else ""
|
|
925
|
+
end
|
|
926
|
+
|
|
927
|
+
display_name = "#{type_badge}#{model_name} (#{masked_key})"
|
|
928
|
+
choices << {
|
|
929
|
+
name: display_name,
|
|
930
|
+
value: { action: :switch, index: idx }
|
|
931
|
+
}
|
|
932
|
+
end
|
|
933
|
+
|
|
934
|
+
# Add action buttons
|
|
935
|
+
choices << { name: "─" * 50, disabled: true }
|
|
936
|
+
choices << { name: "[+] Add New Model", value: { action: :add } }
|
|
937
|
+
choices << { name: "[*] Edit Current Model", value: { action: :edit } }
|
|
938
|
+
choices << { name: "[-] Delete Model", value: { action: :delete } } if current_config.models.length > 1
|
|
939
|
+
choices << { name: "[X] Close", value: { action: :close } }
|
|
940
|
+
|
|
941
|
+
# Show menu
|
|
942
|
+
result = modal.show(title: "Model Configuration", choices: choices)
|
|
943
|
+
|
|
944
|
+
return nil if result.nil?
|
|
945
|
+
|
|
946
|
+
case result[:action]
|
|
947
|
+
when :switch
|
|
948
|
+
current_config.switch_model(result[:index])
|
|
949
|
+
# Auto-save after switching
|
|
950
|
+
current_config.save
|
|
951
|
+
# Return to indicate config changed (need to update client)
|
|
952
|
+
return { action: :switch }
|
|
953
|
+
when :add
|
|
954
|
+
new_model = show_model_edit_form(nil, test_callback: test_callback)
|
|
955
|
+
if new_model
|
|
956
|
+
# Determine anthropic_format based on provider
|
|
957
|
+
# For Anthropic provider, use Anthropic API format
|
|
958
|
+
anthropic_format = new_model[:provider] == "anthropic"
|
|
959
|
+
|
|
960
|
+
current_config.add_model(
|
|
961
|
+
model: new_model[:model],
|
|
962
|
+
api_key: new_model[:api_key],
|
|
963
|
+
base_url: new_model[:base_url],
|
|
964
|
+
anthropic_format: anthropic_format
|
|
965
|
+
)
|
|
966
|
+
# Auto-save after adding
|
|
967
|
+
current_config.save
|
|
968
|
+
# Set newly added model as default
|
|
969
|
+
current_config.switch_model(current_config.models.length - 1)
|
|
970
|
+
current_config.save
|
|
971
|
+
# Return to exit the menu
|
|
972
|
+
return { action: :switch }
|
|
973
|
+
end
|
|
974
|
+
when :edit
|
|
975
|
+
current_model = current_config.current_model
|
|
976
|
+
edited = show_model_edit_form(current_model, test_callback: test_callback)
|
|
977
|
+
if edited
|
|
978
|
+
# Update current model in place (keep anthropic_format unchanged)
|
|
979
|
+
current_model["api_key"] = edited[:api_key]
|
|
980
|
+
current_model["model"] = edited[:model]
|
|
981
|
+
current_model["base_url"] = edited[:base_url]
|
|
982
|
+
# Auto-save after editing
|
|
983
|
+
current_config.save
|
|
984
|
+
# Return to indicate config changed (need to update client)
|
|
985
|
+
return { action: :edit }
|
|
986
|
+
end
|
|
987
|
+
when :delete
|
|
988
|
+
if current_config.models.length <= 1
|
|
989
|
+
# Can't delete - show error and continue
|
|
990
|
+
next
|
|
991
|
+
end
|
|
992
|
+
|
|
993
|
+
# Delete current model
|
|
994
|
+
current_config.remove_model(current_config.current_model_index)
|
|
995
|
+
# Auto-save after deleting
|
|
996
|
+
current_config.save
|
|
997
|
+
when :close
|
|
998
|
+
# Just close the modal
|
|
999
|
+
return nil
|
|
1000
|
+
end
|
|
1001
|
+
end
|
|
1002
|
+
end
|
|
856
1003
|
|
|
1004
|
+
# Show time machine menu for task undo/redo
|
|
1005
|
+
# @param history [Array<Hash>] Task history with format: [{task_id, summary, status, has_branches}]
|
|
1006
|
+
# @return [Integer, nil] Selected task ID or nil if cancelled
|
|
1007
|
+
public def show_time_machine_menu(history)
|
|
1008
|
+
modal = Components::ModalComponent.new
|
|
1009
|
+
|
|
1010
|
+
# Build menu choices from history
|
|
1011
|
+
choices = history.map do |task|
|
|
1012
|
+
# Build visual indicator
|
|
1013
|
+
indicator = if task[:status] == :current
|
|
1014
|
+
"→ " # Current task
|
|
1015
|
+
elsif task[:status] == :future
|
|
1016
|
+
"↯ " # Future task (after undo)
|
|
1017
|
+
else
|
|
1018
|
+
" " # Past task
|
|
1019
|
+
end
|
|
1020
|
+
|
|
1021
|
+
# Add branch indicator
|
|
1022
|
+
indicator += "⎇ " if task[:has_branches]
|
|
1023
|
+
|
|
1024
|
+
# Truncate summary to fit on screen
|
|
1025
|
+
max_summary_length = 60
|
|
1026
|
+
summary = task[:summary]
|
|
1027
|
+
if summary.length > max_summary_length
|
|
1028
|
+
summary = summary[0...max_summary_length] + "..."
|
|
1029
|
+
end
|
|
1030
|
+
|
|
1031
|
+
{
|
|
1032
|
+
name: "#{indicator}Task #{task[:task_id]}: #{summary}",
|
|
1033
|
+
value: task[:task_id]
|
|
1034
|
+
}
|
|
1035
|
+
end
|
|
1036
|
+
|
|
1037
|
+
# Show modal
|
|
1038
|
+
result = modal.show(
|
|
1039
|
+
title: "Time Machine - Select Task to Navigate",
|
|
1040
|
+
choices: choices
|
|
1041
|
+
)
|
|
1042
|
+
|
|
1043
|
+
result # Return selected task_id or nil
|
|
1044
|
+
end
|
|
1045
|
+
|
|
1046
|
+
# Show form for editing a model
|
|
1047
|
+
# @param model [Hash, nil] Existing model hash or nil for new model
|
|
1048
|
+
# @return [Hash, nil] Updated model hash or nil if cancelled
|
|
1049
|
+
private def show_model_edit_form(model, test_callback: nil)
|
|
1050
|
+
modal = Components::ModalComponent.new
|
|
1051
|
+
|
|
1052
|
+
is_new = model.nil?
|
|
1053
|
+
model ||= {}
|
|
1054
|
+
|
|
1055
|
+
# For new models, show provider selection first
|
|
1056
|
+
selected_provider = nil
|
|
1057
|
+
if is_new
|
|
1058
|
+
# Build provider choices
|
|
1059
|
+
provider_choices = Clacky::Providers.list.map do |id, name|
|
|
1060
|
+
{ name: name, value: id }
|
|
1061
|
+
end
|
|
1062
|
+
provider_choices << { name: "─" * 40, disabled: true }
|
|
1063
|
+
provider_choices << { name: "Custom (manual configuration)", value: "custom" }
|
|
1064
|
+
|
|
1065
|
+
# Show provider selection
|
|
1066
|
+
selected_provider = modal.show(
|
|
1067
|
+
title: "Select Provider",
|
|
1068
|
+
choices: provider_choices
|
|
1069
|
+
)
|
|
1070
|
+
|
|
1071
|
+
# User cancelled
|
|
1072
|
+
return nil if selected_provider.nil?
|
|
1073
|
+
end
|
|
1074
|
+
|
|
857
1075
|
# Prepare masked API key for display
|
|
858
|
-
masked_key =
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
1076
|
+
masked_key = mask_api_key(model["api_key"])
|
|
1077
|
+
|
|
1078
|
+
# Pre-fill values from provider preset if selected
|
|
1079
|
+
provider_preset = nil
|
|
1080
|
+
if selected_provider && selected_provider != "custom"
|
|
1081
|
+
provider_preset = Clacky::Providers.get(selected_provider)
|
|
862
1082
|
end
|
|
863
|
-
|
|
1083
|
+
|
|
1084
|
+
# Get default values from provider or existing model
|
|
1085
|
+
default_model = provider_preset ? provider_preset["default_model"] : model["model"]
|
|
1086
|
+
default_base_url = provider_preset ? provider_preset["base_url"] : model["base_url"]
|
|
1087
|
+
default_api_key = model["api_key"] || ""
|
|
1088
|
+
|
|
864
1089
|
# Define fields
|
|
865
1090
|
fields = [
|
|
866
1091
|
{
|
|
867
1092
|
name: :api_key,
|
|
868
|
-
label: "API Key (current: #{masked_key}):",
|
|
1093
|
+
label: "API Key #{is_new ? '' : "(current: #{masked_key})"}:",
|
|
869
1094
|
default: "",
|
|
870
1095
|
mask: true
|
|
871
1096
|
},
|
|
872
1097
|
{
|
|
873
1098
|
name: :model,
|
|
874
|
-
label: "Model (current: #{
|
|
875
|
-
default: ""
|
|
1099
|
+
label: "Model #{is_new && default_model ? "(default: #{default_model})" : (is_new ? '' : "(current: #{model['model']})")}:",
|
|
1100
|
+
default: default_model || ""
|
|
876
1101
|
},
|
|
877
1102
|
{
|
|
878
1103
|
name: :base_url,
|
|
879
|
-
label: "Base URL (current: #{
|
|
880
|
-
default: ""
|
|
1104
|
+
label: "Base URL #{is_new && default_base_url ? "(default: #{default_base_url})" : (is_new ? '' : "(current: #{model['base_url']})")}:",
|
|
1105
|
+
default: default_base_url || ""
|
|
881
1106
|
}
|
|
882
1107
|
]
|
|
883
|
-
|
|
1108
|
+
|
|
884
1109
|
# Create validator if test_callback provided
|
|
885
1110
|
validator = if test_callback
|
|
886
1111
|
lambda do |values|
|
|
887
|
-
# Merge values
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
1112
|
+
# Merge values: use user input if provided, otherwise keep existing model value
|
|
1113
|
+
api_key = values[:api_key].to_s.empty? ? model["api_key"] : values[:api_key]
|
|
1114
|
+
model_name = values[:model].to_s.empty? ? model["model"] : values[:model]
|
|
1115
|
+
base_url = values[:base_url].to_s.empty? ? model["base_url"] : values[:base_url]
|
|
1116
|
+
anthropic_format = model["anthropic_format"] # Not editable in form, use model's value
|
|
1117
|
+
|
|
1118
|
+
test_config_values = {
|
|
1119
|
+
"api_key" => api_key,
|
|
1120
|
+
"model" => model_name,
|
|
1121
|
+
"base_url" => base_url,
|
|
1122
|
+
"anthropic_format" => anthropic_format
|
|
1123
|
+
}
|
|
892
1124
|
|
|
893
|
-
#
|
|
894
|
-
|
|
1125
|
+
# For new models, require all fields
|
|
1126
|
+
if is_new
|
|
1127
|
+
if test_config_values["api_key"].to_s.empty?
|
|
1128
|
+
return { success: false, error: "API Key is required for new model" }
|
|
1129
|
+
end
|
|
1130
|
+
if test_config_values["model"].to_s.empty?
|
|
1131
|
+
return { success: false, error: "Model name is required" }
|
|
1132
|
+
end
|
|
1133
|
+
if test_config_values["base_url"].to_s.empty?
|
|
1134
|
+
return { success: false, error: "Base URL is required" }
|
|
1135
|
+
end
|
|
1136
|
+
end
|
|
1137
|
+
|
|
1138
|
+
# Create a temporary config for testing
|
|
1139
|
+
temp_config = Clacky::AgentConfig.new(models: [test_config_values], current_model_index: 0)
|
|
1140
|
+
test_callback.call(temp_config)
|
|
895
1141
|
end
|
|
896
1142
|
else
|
|
897
1143
|
nil
|
|
898
1144
|
end
|
|
899
|
-
|
|
1145
|
+
|
|
1146
|
+
# Determine modal title based on provider
|
|
1147
|
+
modal_title = if is_new && selected_provider && selected_provider != "custom"
|
|
1148
|
+
provider_name = Clacky::Providers.get(selected_provider)&.dig("name") || selected_provider
|
|
1149
|
+
"Add #{provider_name} Model"
|
|
1150
|
+
elsif is_new
|
|
1151
|
+
"Add Custom Model"
|
|
1152
|
+
else
|
|
1153
|
+
"Edit Model"
|
|
1154
|
+
end
|
|
1155
|
+
|
|
900
1156
|
# Show modal and collect values
|
|
901
|
-
modal.show(
|
|
1157
|
+
result = modal.show(
|
|
1158
|
+
title: modal_title,
|
|
1159
|
+
fields: fields,
|
|
1160
|
+
validator: validator
|
|
1161
|
+
)
|
|
1162
|
+
|
|
1163
|
+
return nil if result.nil?
|
|
1164
|
+
|
|
1165
|
+
# Merge with existing model values or provider defaults
|
|
1166
|
+
{
|
|
1167
|
+
api_key: result[:api_key].to_s.empty? ? model["api_key"] : result[:api_key],
|
|
1168
|
+
model: result[:model].to_s.empty? ? (model["model"] || default_model) : result[:model],
|
|
1169
|
+
base_url: result[:base_url].to_s.empty? ? (model["base_url"] || default_base_url) : result[:base_url],
|
|
1170
|
+
provider: selected_provider
|
|
1171
|
+
}
|
|
1172
|
+
end
|
|
1173
|
+
|
|
1174
|
+
# Mask API key for display
|
|
1175
|
+
private def mask_api_key(api_key)
|
|
1176
|
+
if api_key && !api_key.empty?
|
|
1177
|
+
"#{api_key[0..5]}...#{api_key[-4..]}"
|
|
1178
|
+
else
|
|
1179
|
+
"not set"
|
|
1180
|
+
end
|
|
902
1181
|
end
|
|
903
1182
|
end
|
|
904
1183
|
end
|
data/lib/clacky/ui2.rb
CHANGED
|
@@ -3,6 +3,8 @@
|
|
|
3
3
|
# UI2 - MVC-based terminal UI system for Clacky
|
|
4
4
|
# Provides split-screen interface with scrollable output and fixed input
|
|
5
5
|
|
|
6
|
+
require_relative "ui2/thinking_verbs"
|
|
7
|
+
require_relative "ui2/progress_indicator"
|
|
6
8
|
require_relative "ui2/theme_manager"
|
|
7
9
|
require_relative "ui2/screen_buffer"
|
|
8
10
|
require_relative "ui2/layout_manager"
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Clacky
|
|
4
|
+
# UIInterface defines the standard interface between Agent/CLI and UI implementations.
|
|
5
|
+
# All UI controllers (UIController, JsonUIController) must implement these methods.
|
|
6
|
+
module UIInterface
|
|
7
|
+
# === Output display ===
|
|
8
|
+
def show_user_message(content, images: []); end
|
|
9
|
+
def show_assistant_message(content); end
|
|
10
|
+
def show_tool_call(name, args); end
|
|
11
|
+
def show_tool_result(result); end
|
|
12
|
+
def show_tool_error(error); end
|
|
13
|
+
def show_tool_args(formatted_args); end
|
|
14
|
+
def show_file_write_preview(path, is_new_file:); end
|
|
15
|
+
def show_file_edit_preview(path); end
|
|
16
|
+
def show_file_error(error_message); end
|
|
17
|
+
def show_shell_preview(command); end
|
|
18
|
+
def show_diff(old_content, new_content, max_lines: 50); end
|
|
19
|
+
def show_token_usage(token_data); end
|
|
20
|
+
def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false); end
|
|
21
|
+
def append_output(content); end
|
|
22
|
+
|
|
23
|
+
# === Status messages ===
|
|
24
|
+
def show_info(message, prefix_newline: true); end
|
|
25
|
+
def show_warning(message); end
|
|
26
|
+
def show_error(message); end
|
|
27
|
+
def show_success(message); end
|
|
28
|
+
def log(message, level: :info); end
|
|
29
|
+
|
|
30
|
+
# === Progress ===
|
|
31
|
+
def show_progress(message = nil, prefix_newline: true); end
|
|
32
|
+
def clear_progress; end
|
|
33
|
+
|
|
34
|
+
# === State updates ===
|
|
35
|
+
def update_sessionbar(tasks: nil, cost: nil, status: nil); end
|
|
36
|
+
def update_todos(todos); end
|
|
37
|
+
def set_working_status; end
|
|
38
|
+
def set_idle_status; end
|
|
39
|
+
|
|
40
|
+
# === Blocking interaction ===
|
|
41
|
+
def request_confirmation(message, default: true); end
|
|
42
|
+
|
|
43
|
+
# === Input control (CLI layer) ===
|
|
44
|
+
def clear_input; end
|
|
45
|
+
def set_input_tips(message, type: :info); end
|
|
46
|
+
|
|
47
|
+
# === Lifecycle ===
|
|
48
|
+
def stop; end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
@@ -29,14 +29,42 @@ module Clacky
|
|
|
29
29
|
|
|
30
30
|
private
|
|
31
31
|
|
|
32
|
-
# Simple JSON repair: complete brackets and quotes
|
|
32
|
+
# Simple JSON repair: complete brackets and quotes, and remove XML contamination
|
|
33
33
|
def self.repair_json(json_str)
|
|
34
34
|
result = json_str.strip
|
|
35
35
|
|
|
36
|
-
#
|
|
36
|
+
# Step 1: Remove XML-style parameter tags that Claude might mix in
|
|
37
|
+
# Pattern 1: </parameter> closing tags - remove completely
|
|
38
|
+
result = result.gsub(/<\/parameter>/, '')
|
|
39
|
+
|
|
40
|
+
# Pattern 2: <parameter name="key"> or <parameter name="key": opening tags -> convert to JSON key
|
|
41
|
+
# Example: \n<parameter name="end_line"> 330 -> , "end_line": 330
|
|
42
|
+
# Also handles: \n<parameter name="end_line": 330 -> , "end_line": 330
|
|
43
|
+
result = result.gsub(/<parameter\s+name="([^"]+)":\s*/) { |match| ", \"#{$1}\": " }
|
|
44
|
+
result = result.gsub(/<parameter\s+name="([^"]+)">/) { |match| ", \"#{$1}\":" }
|
|
45
|
+
|
|
46
|
+
# Pattern 3: Remove any remaining XML-like tags
|
|
47
|
+
result = result.gsub(/<[^>]+>/, '')
|
|
48
|
+
|
|
49
|
+
# Step 2: Clean up newlines with commas
|
|
50
|
+
# Example: 315\n, "end_line" -> 315, "end_line"
|
|
51
|
+
result = result.gsub(/\n\s*,/, ',')
|
|
52
|
+
result = result.gsub(/,\s*\n/, ',')
|
|
53
|
+
|
|
54
|
+
# Step 3: Clean up formatting issues
|
|
55
|
+
# Remove multiple consecutive commas
|
|
56
|
+
result = result.gsub(/,+/, ',')
|
|
57
|
+
# Remove trailing commas before closing braces/brackets
|
|
58
|
+
result = result.gsub(/,\s*}/, '}')
|
|
59
|
+
result = result.gsub(/,\s*\]/, ']')
|
|
60
|
+
# Remove leading commas after opening braces/brackets
|
|
61
|
+
result = result.gsub(/\{\s*,/, '{')
|
|
62
|
+
result = result.gsub(/\[\s*,/, '[')
|
|
63
|
+
|
|
64
|
+
# Step 4: Complete unclosed strings
|
|
37
65
|
result += '"' if result.count('"').odd?
|
|
38
66
|
|
|
39
|
-
# Complete unclosed braces
|
|
67
|
+
# Step 5: Complete unclosed braces
|
|
40
68
|
depth = 0
|
|
41
69
|
result.each_char { |c| depth += 1 if c == '{'; depth -= 1 if c == '}' }
|
|
42
70
|
result += '}' * depth if depth > 0
|
|
@@ -166,35 +166,30 @@ module Clacky
|
|
|
166
166
|
end
|
|
167
167
|
|
|
168
168
|
# Check if file is binary (not text)
|
|
169
|
-
# @param data [String] File content
|
|
169
|
+
# @param data [String] File content (should be read in binary mode, encoding: ASCII-8BIT)
|
|
170
170
|
# @param sample_size [Integer] Number of bytes to check (default: 8192)
|
|
171
171
|
# @return [Boolean] True if file appears to be binary
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
172
|
+
#
|
|
173
|
+
# Strategy: only trust known magic byte signatures.
|
|
174
|
+
# We intentionally avoid heuristics (byte-ratio, UTF-8 validity, etc.) because
|
|
175
|
+
# they produce false positives on legitimate text files containing multibyte
|
|
176
|
+
# characters (e.g. Chinese, Japanese). Occasionally missing an unlabelled binary
|
|
177
|
+
# is acceptable; misclassifying a real text file is not.
|
|
178
|
+
def binary_file?(data, sample_size: 8192)
|
|
179
|
+
sample = data.b[0, sample_size] || ""
|
|
175
180
|
return false if sample.empty?
|
|
176
181
|
|
|
177
|
-
# Check for known binary signatures
|
|
182
|
+
# Check for known binary file signatures (magic bytes)
|
|
178
183
|
FILE_SIGNATURES.each do |signature, _format|
|
|
179
184
|
return true if sample.start_with?(signature)
|
|
180
185
|
end
|
|
181
186
|
|
|
182
|
-
# Check for WebP (RIFF
|
|
183
|
-
if sample.start_with?("RIFF".b) && sample.
|
|
187
|
+
# Check for WebP (RIFF....WEBP header)
|
|
188
|
+
if sample.start_with?("RIFF".b) && sample.bytesize >= 12 && sample[8..11] == "WEBP".b
|
|
184
189
|
return true
|
|
185
190
|
end
|
|
186
191
|
|
|
187
|
-
|
|
188
|
-
# This prevents short outputs from being incorrectly flagged as binary
|
|
189
|
-
return false if sample.size < min_binary_length
|
|
190
|
-
|
|
191
|
-
# Count non-printable characters (excluding common whitespace)
|
|
192
|
-
non_printable = sample.bytes.count do |byte|
|
|
193
|
-
byte < 32 && ![9, 10, 13].include?(byte) || byte >= 127
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# If more than 30% non-printable, consider it binary
|
|
197
|
-
(non_printable.to_f / sample.size) > 0.3
|
|
192
|
+
false
|
|
198
193
|
end
|
|
199
194
|
|
|
200
195
|
# Check if a file at the given path is binary (not text)
|
data/lib/clacky/version.rb
CHANGED