openclacky 1.3.3 → 1.3.4

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/docs/rich_ui_guide.md +277 -0
  4. data/docs/rich_ui_refactor_plan.md +396 -0
  5. data/lib/clacky/agent/llm_caller.rb +10 -4
  6. data/lib/clacky/agent/session_serializer.rb +3 -2
  7. data/lib/clacky/agent.rb +3 -2
  8. data/lib/clacky/agent_config.rb +2 -14
  9. data/lib/clacky/api_extension.rb +262 -0
  10. data/lib/clacky/api_extension_loader.rb +156 -0
  11. data/lib/clacky/cli.rb +93 -3
  12. data/lib/clacky/client.rb +38 -13
  13. data/lib/clacky/default_agents/_panels/git/panel.js +1 -1
  14. data/lib/clacky/default_agents/_panels/time_machine/panel.js +1 -1
  15. data/lib/clacky/default_skills/media-gen/SKILL.md +9 -6
  16. data/lib/clacky/idle_compression_timer.rb +3 -1
  17. data/lib/clacky/locales/en.rb +26 -0
  18. data/lib/clacky/locales/i18n.rb +26 -0
  19. data/lib/clacky/locales/zh.rb +26 -0
  20. data/lib/clacky/rich_ui/components/base_component.rb +50 -0
  21. data/lib/clacky/rich_ui/components/dialogs/approval_dialog.rb +142 -0
  22. data/lib/clacky/rich_ui/components/dialogs/config_menu_dialog.rb +106 -0
  23. data/lib/clacky/rich_ui/components/dialogs/form_dialog.rb +128 -0
  24. data/lib/clacky/rich_ui/components/sidebar.rb +119 -0
  25. data/lib/clacky/rich_ui/components/sidebar_panels.rb +134 -0
  26. data/lib/clacky/rich_ui/components/status_view.rb +58 -0
  27. data/lib/clacky/rich_ui/components/thinking_live_view.rb +79 -0
  28. data/lib/clacky/rich_ui/entry_tracker.rb +56 -0
  29. data/lib/clacky/rich_ui/layout_adapter.rb +16 -0
  30. data/lib/clacky/rich_ui/progress_handle_adapter.rb +24 -0
  31. data/lib/clacky/rich_ui/rich_ui_controller.rb +868 -0
  32. data/lib/clacky/rich_ui/shell/rich_agent_shell.rb +184 -0
  33. data/lib/clacky/rich_ui/view_renderer.rb +291 -0
  34. data/lib/clacky/rich_ui.rb +57 -0
  35. data/lib/clacky/rich_ui_controller.rb +3 -1549
  36. data/lib/clacky/server/api_extension_dispatcher.rb +120 -0
  37. data/lib/clacky/server/http_server.rb +150 -103
  38. data/lib/clacky/server/session_registry.rb +1 -1
  39. data/lib/clacky/shell_hook_loader.rb +1 -1
  40. data/lib/clacky/tools/edit.rb +14 -2
  41. data/lib/clacky/ui2/ui_controller.rb +7 -0
  42. data/lib/clacky/version.rb +1 -1
  43. data/lib/clacky/web/app.css +56 -59
  44. data/lib/clacky/web/app.js +65 -7
  45. data/lib/clacky/web/components/onboard.js +18 -2
  46. data/lib/clacky/web/core/aside.js +8 -3
  47. data/lib/clacky/web/core/ext.js +1 -1
  48. data/lib/clacky/web/features/skills/store.js +30 -2
  49. data/lib/clacky/web/features/skills/view.js +32 -1
  50. data/lib/clacky/web/features/workspace/view.js +1 -1
  51. data/lib/clacky/web/i18n.js +32 -20
  52. data/lib/clacky/web/index.html +9 -17
  53. data/lib/clacky/web/sessions.js +286 -28
  54. data/lib/clacky/web/settings.js +109 -111
  55. data/lib/clacky/web/ws-dispatcher.js +7 -3
  56. data/lib/clacky.rb +17 -2
  57. metadata +38 -2
  58. data/lib/clacky/media/output_dir.rb +0 -43
@@ -0,0 +1,184 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby_rich"
4
+ require_relative "../components/sidebar"
5
+ require_relative "../components/thinking_live_view"
6
+ require_relative "../components/status_view"
7
+
8
+ module Clacky
9
+ class RichAgentShell < RubyRich::AgentShell
10
+ attr_reader :thinking_live, :sidebar
11
+ attr_accessor :clacky_controller
12
+ attr_reader :callbacks
13
+
14
+ def build_layout
15
+ @sidebar = RichUI::RichSidebar.new
16
+ @thinking_live = RichUI::ThinkingLiveView.new(self)
17
+ @viewport.instance_variable_set(:@scrollbar, false)
18
+ @viewport.instance_variable_set(:@auto_copy, false)
19
+ @viewport.instance_variable_set(:@drag_mode, :selection)
20
+
21
+ # Patch Viewport#copy_selection to also clear the visual selection
22
+ # highlight. The upstream copy_selection copies text to the clipboard
23
+ # but leaves @selection_start / @selection_end intact, so the
24
+ # inverted-colour highlight survives both right-click and Ctrl+C.
25
+ vp = @viewport
26
+ vp.define_singleton_method(:copy_selection) do
27
+ text = @selected_text.to_s
28
+ return false if text.empty?
29
+
30
+ copy_to_clipboard(text)
31
+ @selection_start = nil
32
+ @selection_end = nil
33
+ @selected_text = ""
34
+ true
35
+ end
36
+ root = RubyRich::Layout.new(name: :root)
37
+ root.split_column(
38
+ RubyRich::Layout.new(name: :header, size: 1),
39
+ RubyRich::Layout.new(name: :body, ratio: 1),
40
+ RubyRich::Layout.new(name: :composer, size: 6),
41
+ RubyRich::Layout.new(name: :status, size: 1)
42
+ )
43
+
44
+ main_area = RubyRich::Layout.new(name: :main, ratio: 1)
45
+ main_area.split_column(
46
+ RubyRich::Layout.new(name: :transcript, ratio: 1),
47
+ RubyRich::Layout.new(name: :thinking_live, size: 0)
48
+ )
49
+
50
+ root[:body].split_row(
51
+ main_area,
52
+ RubyRich::Layout.new(name: :todos, size: 36)
53
+ )
54
+
55
+ root[:header].content = RubyRich::AppShell::HeaderView.new(self)
56
+ root[:transcript].content = @viewport
57
+ root[:todos].content = @sidebar
58
+ root[:thinking_live].content = @thinking_live
59
+ root[:composer].content = RubyRich::AppShell::FramedView.new(@composer, title: "Composer", theme: @theme) { @composer.focused? }
60
+ root[:status].content = RichUI::RichStatusView.new(self)
61
+ root
62
+ end
63
+
64
+ def attach_components
65
+ @viewport.attach(@layout[:transcript])
66
+ @transcript.attach(@layout[:transcript])
67
+ @composer.focus.attach(@layout[:composer])
68
+
69
+ @focus_manager
70
+ .register(:transcript, @layout[:transcript], RubyRich::AppShell::FocusTarget.new(@transcript, @viewport))
71
+ .register(:composer, @layout[:composer], @composer)
72
+ .attach(@layout)
73
+ @focus_manager.focus(:composer)
74
+
75
+ @layout.key(:ctrl_c, 1_000) do |_event, live|
76
+ live.stop if @stop_on_ctrl_c != false
77
+ false
78
+ end
79
+ end
80
+
81
+ def attach_agent_controls
82
+ @composer.instance_variable_set(:@on_interrupt, nil)
83
+ # Register /model command
84
+ shell_ref = self
85
+ @composer.register_command(name: "/model", description: "Switch LLM model",
86
+ handler: -> { shell_ref.callbacks[:model_switch]&.call })
87
+ # Wire vim scroll callback: j/k in single-line normal mode scrolls transcript
88
+ @composer.instance_variable_set(:@on_vim_scroll, ->(delta) { @viewport.scroll_by(delta) })
89
+ # Inject Esc cancellation stack via singleton method on the Composer instance.
90
+ # This avoids both the Layout @event_intercepted bug and monkey-patch complexity.
91
+ native_escape = @composer.method(:escape)
92
+ shell = self
93
+ @composer.define_singleton_method(:escape) do
94
+ handled = shell.callbacks[:esc]&.call || false
95
+ handled ? nil : native_escape.call
96
+ end
97
+ # Clear Ctrl+C warning as soon as the user starts typing
98
+ native_insert = @composer.method(:insert_text)
99
+ @composer.define_singleton_method(:insert_text) do |text|
100
+ shell.callbacks[:clear_ctrlc]&.call
101
+ native_insert.call(text)
102
+ end
103
+
104
+ @layout.key(:ctrl_c, 2_000) do |_event, live|
105
+ handle_interrupt(live, self)
106
+ false
107
+ end
108
+
109
+ @layout.key(:ctrl_m, 2_000) do |_event, _live|
110
+ toggle_permission_mode
111
+ false
112
+ end
113
+
114
+ # Tab toggles permission mode (overrides FocusManager's focus cycling)
115
+ @layout.key(:tab, 600) do |_event, _live|
116
+ toggle_permission_mode
117
+ false
118
+ end
119
+ # Re-focus composer AFTER FocusManager (priority 500) has cycled focus.
120
+ # Also suppress Composer's own tab handler (priority 200) which would
121
+ # otherwise fire open_menu_if_available.
122
+ @layout.key(:tab, 100) do |_event, _live|
123
+ @composer.instance_variable_set(:@ignore_next_tab, true)
124
+ @focus_manager.focus(:composer)
125
+ false
126
+ end
127
+
128
+ # Sidebar panel shortcuts (F1-F4)
129
+ @layout.key(:f1, 1_500) do |_event, _live|
130
+ @sidebar.set_mode(:work)
131
+ false
132
+ end
133
+ @layout.key(:f2, 1_500) do |_event, _live|
134
+ @sidebar.set_mode(:tasks)
135
+ false
136
+ end
137
+ @layout.key(:f3, 1_500) do |_event, _live|
138
+ @sidebar.set_mode(:auto)
139
+ false
140
+ end
141
+ @layout.key(:f4, 1_500) do |_event, _live|
142
+ @sidebar.set_mode(:context)
143
+ false
144
+ end
145
+ end
146
+
147
+ def handle_interrupt(_live = nil, _source = nil)
148
+ return false if copy_viewport_selection
149
+
150
+ input_was_empty = @composer.value.to_s.empty?
151
+ @callbacks[:interrupt]&.call(input_was_empty: input_was_empty)
152
+ false
153
+ end
154
+
155
+ def copy_viewport_selection
156
+ if @viewport.instance_variable_get(:@selecting)
157
+ @viewport.send(:stop_selection)
158
+ end
159
+
160
+ @viewport.copy_selection
161
+ end
162
+
163
+ def toggle_permission_mode
164
+ current = @callbacks[:mode_toggle] ? @mode : :confirm_safes
165
+ # Toggle between confirm_safes and confirm_all
166
+ new_mode = current.to_s == "confirm_all" ? "confirm_safes" : "confirm_all"
167
+ @mode = new_mode.to_sym
168
+ @callbacks[:mode_toggle]&.call(@mode)
169
+ @status = "mode · #{@mode}"
170
+ @focus_manager.focus(:composer)
171
+ end
172
+
173
+ def on_esc(&block)
174
+ @callbacks[:esc] = block
175
+ self
176
+ end
177
+
178
+ private :build_layout,
179
+ :attach_components,
180
+ :attach_agent_controls,
181
+ :handle_interrupt,
182
+ :copy_viewport_selection
183
+ end
184
+ end
@@ -0,0 +1,291 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require "uri"
5
+ require "ruby_rich"
6
+
7
+ module Clacky
8
+ module RichUI
9
+ # ViewRenderer provides stateless formatting helpers extracted from
10
+ # RichUIController. All methods are module functions — callable as
11
+ # ViewRenderer.format_args(...)
12
+ # or mixin-able via `include ViewRenderer`.
13
+ module ViewRenderer
14
+ module_function
15
+
16
+ # ── Tool output formatting ──────────────────────────────────
17
+
18
+ def format_tool_output(text, status = :done)
19
+ marker = status == :error ? "[Error]" : "[OK]"
20
+ color = status == :error ? :red : :green
21
+ clean = text.to_s.sub(/\A\[(?:OK|Error)\]\s*/, "")
22
+ "#{RubyRich::AnsiCode.color(color, true)}#{marker}#{RubyRich::AnsiCode.reset} #{clean}"
23
+ end
24
+
25
+ def format_args(args)
26
+ data = args.is_a?(String) ? (JSON.parse(args) rescue args) : args
27
+ return data.to_s unless data.is_a?(Hash) && !data.empty?
28
+
29
+ data.map { |k, v| "#{k}: #{format_tool_value(v)}" }.join("\n")
30
+ end
31
+
32
+ def format_tool_value(v)
33
+ v.is_a?(String) ? v : JSON.generate(v)
34
+ end
35
+
36
+ def normalize_todo(todo)
37
+ case todo
38
+ when Hash
39
+ title = todo[:content] || todo["content"] || todo[:title] || todo["title"] || todo[:task] || todo["task"]
40
+ status = todo[:status] || todo["status"] || :pending
41
+ { label: title.to_s, title: title.to_s, status: status.to_sym }
42
+ else
43
+ { label: todo.to_s, title: todo.to_s, status: :pending }
44
+ end
45
+ end
46
+
47
+ # ── Tool activity label helpers ─────────────────────────────
48
+
49
+ def tool_activity_label(name, args)
50
+ tool_name = name.to_s
51
+ data = normalize_tool_args(args)
52
+
53
+ case tool_name
54
+ when "web_search"
55
+ query = data["query"].to_s
56
+ return tool_name if query.empty?
57
+
58
+ %(web_search("#{escape_tool_label(truncate_tool_label(query))}"))
59
+ when "web_fetch"
60
+ url = data["url"].to_s
61
+ return tool_name if url.empty?
62
+
63
+ "web_fetch(#{truncate_tool_label(tool_url_host(url))})"
64
+ else
65
+ compact = compact_tool_arg(data)
66
+ compact ? "#{tool_name}(#{compact})" : tool_name
67
+ end
68
+ end
69
+
70
+ def normalize_tool_args(args)
71
+ parsed = if args.is_a?(String)
72
+ JSON.parse(args)
73
+ else
74
+ args
75
+ end
76
+ return {} unless parsed.is_a?(Hash)
77
+
78
+ parsed.each_with_object({}) { |(key, value), hash| hash[key.to_s] = value }
79
+ rescue JSON::ParserError
80
+ {}
81
+ end
82
+
83
+ def compact_tool_arg(data)
84
+ key = %w[query url path file command pattern task].find { |candidate| data.key?(candidate) && !data[candidate].to_s.empty? }
85
+ return nil unless key
86
+
87
+ value = key == "url" ? tool_url_host(data[key].to_s) : data[key].to_s
88
+ escaped = escape_tool_label(truncate_tool_label(value))
89
+ value.match?(/\A[\w.-]+\z/) ? escaped : %("#{escaped}")
90
+ end
91
+
92
+ def tool_url_host(url)
93
+ URI.parse(url).host || url
94
+ rescue URI::InvalidURIError
95
+ url
96
+ end
97
+
98
+ def truncate_tool_label(text, limit = 40)
99
+ chars = text.to_s.each_char.to_a
100
+ return text.to_s if chars.length <= limit
101
+
102
+ "#{chars.first(limit - 3).join}..."
103
+ end
104
+
105
+ def escape_tool_label(text)
106
+ text.to_s.gsub("\\", "\\\\\\").gsub('"', '\"')
107
+ end
108
+
109
+ # ── Markdown helpers ────────────────────────────────────────
110
+
111
+ def normalize_markdown_for_terminal(text)
112
+ text.to_s
113
+ .gsub(/\r\n?/, "\n")
114
+ .gsub(/\A[ \t]*\n+/, "")
115
+ .gsub(/\n+[ \t]*\z/, "")
116
+ end
117
+
118
+ def expand_ansi_multiline_spans(text)
119
+ active = +""
120
+ text.to_s.lines.map do |line|
121
+ body = line.chomp
122
+ prefix = body.start_with?("\e[") || active.empty? ? "" : active
123
+ body.scan(/\e\[[0-9;:]*m/).each do |code|
124
+ active = code == RubyRich::AnsiCode.reset ? +"" : code
125
+ end
126
+ suffix = !active.empty? && !body.end_with?(RubyRich::AnsiCode.reset) ? RubyRich::AnsiCode.reset : ""
127
+ "#{prefix}#{body}#{suffix}"
128
+ end.join("\n")
129
+ end
130
+
131
+ # ── Diff / stats helpers ────────────────────────────────────
132
+
133
+ def parse_diff_stats(diff_text)
134
+ adds = 0
135
+ dels = 0
136
+ hunks = 0
137
+ diff_text.each_line do |line|
138
+ adds += 1 if line.start_with?("+") && !line.start_with?("+++")
139
+ dels += 1 if line.start_with?("-") && !line.start_with?("---")
140
+ hunks += 1 if line.start_with?("@@")
141
+ end
142
+ return "" if adds.zero? && dels.zero?
143
+
144
+ parts = []
145
+ parts << "+#{adds}" if adds.positive?
146
+ parts << "-#{dels}" if dels.positive?
147
+ parts << "#{hunks} hunks" if hunks.positive?
148
+ " (#{parts.join(", ")})"
149
+ end
150
+
151
+ def extract_thinking_and_content(content)
152
+ return ["", content.to_s] if content.nil?
153
+
154
+ thinking_parts = []
155
+ clean = content.to_s.dup
156
+
157
+ clean.gsub!(%r{<think(?:ing)?>\s*([\s\S]*?)\s*</think(?:ing)?>}mi) do
158
+ thinking_parts << Regexp.last_match(1).strip
159
+ ""
160
+ end
161
+
162
+ clean = clean.gsub(/\n{3,}/, "\n\n").strip
163
+ [thinking_parts.join("\n\n"), clean]
164
+ end
165
+
166
+ # ── Config / dialog helpers ─────────────────────────────────
167
+
168
+ def mask_api_key(api_key)
169
+ key = api_key.to_s
170
+ return "not set" if key.empty?
171
+
172
+ "#{key[0..5]}...#{key[-4..]}"
173
+ end
174
+
175
+ def config_menu_choices(current_config)
176
+ choices = current_config.models.each_with_index.map do |model, index|
177
+ type_badge = case model["type"]
178
+ when "default" then "[default] "
179
+ when "lite" then "[lite] "
180
+ else ""
181
+ end
182
+ {
183
+ label: "#{type_badge}#{model["model"] || "unnamed"} (#{mask_api_key(model["api_key"])})",
184
+ value: { action: :switch, model_id: model["id"] },
185
+ current: index == current_config.current_model_index
186
+ }
187
+ end
188
+
189
+ choices + [
190
+ { label: "─" * 50, disabled: true },
191
+ { label: "[+] Add New Model", value: { action: :add } },
192
+ { label: "[*] Edit Current Model", value: { action: :edit } },
193
+ (current_config.models.length > 1 ? { label: "[-] Delete Model", value: { action: :delete } } : nil),
194
+ { label: "[X] Close", value: { action: :close } }
195
+ ].compact
196
+ end
197
+
198
+ def config_initial_selection(choices)
199
+ choices.index { |choice| choice[:current] } || choices.index { |choice| !choice[:disabled] } || 0
200
+ end
201
+
202
+ def merge_model_form_values(result, model:, default_model:, default_base_url:)
203
+ {
204
+ api_key: result[:api_key].to_s.empty? ? model["api_key"] : result[:api_key],
205
+ model: result[:model].to_s.empty? ? (model["model"] || default_model) : result[:model],
206
+ base_url: result[:base_url].to_s.empty? ? (model["base_url"] || default_base_url) : result[:base_url]
207
+ }
208
+ end
209
+
210
+ def validate_model_form(values, is_new:, existing_model:, test_callback:)
211
+ if is_new
212
+ return { success: false, error: "API Key is required for new model" } if values[:api_key].to_s.empty?
213
+ return { success: false, error: "Model name is required" } if values[:model].to_s.empty?
214
+ return { success: false, error: "Base URL is required" } if values[:base_url].to_s.empty?
215
+ end
216
+
217
+ return { success: true } unless test_callback
218
+
219
+ temp_config = Clacky::AgentConfig.new(
220
+ models: [{
221
+ "api_key" => values[:api_key],
222
+ "model" => values[:model],
223
+ "base_url" => values[:base_url],
224
+ "anthropic_format" => existing_model["anthropic_format"]
225
+ }],
226
+ current_model_index: 0
227
+ )
228
+ test_callback.call(temp_config)
229
+ end
230
+
231
+ # ── Approval helpers ────────────────────────────────────────
232
+
233
+ def parse_tool_info(message)
234
+ return [nil, {}] unless message
235
+
236
+ tool_name = message[/\A\w+/]&.downcase
237
+ params = {}
238
+
239
+ case tool_name
240
+ when "edit", "write"
241
+ path = message[/\((.+?)\)/, 1]
242
+ params[:path] = path if path
243
+ when "terminal", "shell", "exec"
244
+ cmd = message[/"(.+?)"/, 1]
245
+ params[:command] = cmd if cmd
246
+ when "web_search", "web_fetch"
247
+ params[:query] = message[(message.index("(")&.+(1) || 0)..]&.chomp(")")&.strip
248
+ when "execute", "run"
249
+ params[:command] = message[(message.index("(")&.+(1) || 0)..]&.chomp(")")&.strip
250
+ end
251
+
252
+ params.reject! { |_, v| v.to_s.empty? }
253
+ [tool_name, params]
254
+ end
255
+
256
+ def tool_risk_level(tool_name)
257
+ case tool_name
258
+ when "read", "grep", "list", "search", "web_search", "web_fetch", "fetch_url"
259
+ :low
260
+ when "edit", "write", "patch", "apply_patch"
261
+ :medium
262
+ when "shell", "terminal", "exec", "execute", "run"
263
+ :high
264
+ when "install", "remove", "delete", "rm", "force"
265
+ :critical
266
+ else
267
+ :medium
268
+ end
269
+ end
270
+
271
+ def tool_category(tool_name)
272
+ case tool_name
273
+ when "read", "write", "edit", "patch", "apply_patch", "grep", "list"
274
+ :file
275
+ when "shell", "terminal", "exec", "execute", "run"
276
+ :shell
277
+ when "web_search", "web_fetch", "fetch_url"
278
+ :network
279
+ when "install", "billing", "payment"
280
+ :paid
281
+ else
282
+ :file
283
+ end
284
+ end
285
+
286
+ def build_fingerprint(tool_name, params)
287
+ "#{tool_name}:#{params.sort.to_s}"
288
+ end
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RichUI - RubyRich-backed TUI system for Clacky
4
+ # Entry point that loads all RichUI modules.
5
+
6
+ require "ruby_rich"
7
+
8
+ module Clacky
9
+ module RichUI
10
+ module ViewportSelectionPatch
11
+ def highlight_display_range(line, start_col, end_col)
12
+ end_col = [end_col, visible_text_width(line.to_s.rstrip)].min
13
+ return line if end_col <= start_col
14
+
15
+ super(line, start_col, end_col)
16
+ end
17
+
18
+ private def visible_text_width(line)
19
+ width = 0
20
+ in_escape = false
21
+
22
+ line.each_char do |char|
23
+ if in_escape
24
+ in_escape = false if char == "m"
25
+ next
26
+ end
27
+
28
+ if char.ord == 27
29
+ in_escape = true
30
+ next
31
+ end
32
+
33
+ width += Unicode::DisplayWidth.of(char)
34
+ end
35
+
36
+ width
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ RubyRich::Viewport.prepend(Clacky::RichUI::ViewportSelectionPatch)
43
+
44
+ require_relative "rich_ui/components/base_component"
45
+ require_relative "rich_ui/view_renderer"
46
+ require_relative "rich_ui/entry_tracker"
47
+ require_relative "rich_ui/components/sidebar_panels"
48
+ require_relative "rich_ui/components/sidebar"
49
+ require_relative "rich_ui/components/thinking_live_view"
50
+ require_relative "rich_ui/components/status_view"
51
+ require_relative "rich_ui/shell/rich_agent_shell"
52
+ require_relative "rich_ui/layout_adapter"
53
+ require_relative "rich_ui/progress_handle_adapter"
54
+ require_relative "rich_ui/components/dialogs/config_menu_dialog"
55
+ require_relative "rich_ui/components/dialogs/form_dialog"
56
+ require_relative "rich_ui/components/dialogs/approval_dialog"
57
+ require_relative "rich_ui/rich_ui_controller"