Module: Clacky::RichUI::ViewRenderer
- Included in:
- Clacky::RichUIController
- Defined in:
- lib/clacky/rich_ui/view_renderer.rb
Overview
ViewRenderer provides stateless formatting helpers extracted from RichUIController. All methods are module functions — callable as
ViewRenderer.format_args(...)
or mixin-able via ‘include ViewRenderer`.
Class Method Summary collapse
- .build_fingerprint(tool_name, params) ⇒ Object
- .compact_tool_arg(data) ⇒ Object
- .config_initial_selection(choices) ⇒ Object
- .config_menu_choices(current_config) ⇒ Object
- .escape_tool_label(text) ⇒ Object
- .expand_ansi_multiline_spans(text) ⇒ Object
- .extract_thinking_and_content(content) ⇒ Object
- .format_args(args) ⇒ Object
-
.format_tool_output(text, status = :done) ⇒ Object
── Tool output formatting ──────────────────────────────────.
- .format_tool_value(v) ⇒ Object
-
.mask_api_key(api_key) ⇒ Object
── Config / dialog helpers ─────────────────────────────────.
- .merge_model_form_values(result, model:, default_model:, default_base_url:) ⇒ Object
-
.normalize_markdown_for_terminal(text) ⇒ Object
── Markdown helpers ────────────────────────────────────────.
- .normalize_todo(todo) ⇒ Object
- .normalize_tool_args(args) ⇒ Object
-
.parse_diff_stats(diff_text) ⇒ Object
── Diff / stats helpers ────────────────────────────────────.
-
.parse_tool_info(message) ⇒ Object
── Approval helpers ────────────────────────────────────────.
-
.tool_activity_label(name, args) ⇒ Object
── Tool activity label helpers ─────────────────────────────.
- .tool_category(tool_name) ⇒ Object
- .tool_risk_level(tool_name) ⇒ Object
- .tool_url_host(url) ⇒ Object
- .truncate_tool_label(text, limit = 40) ⇒ Object
- .validate_model_form(values, is_new:, existing_model:, test_callback:) ⇒ Object
Class Method Details
.build_fingerprint(tool_name, params) ⇒ Object
286 287 288 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 286 def build_fingerprint(tool_name, params) "#{tool_name}:#{params.sort.to_s}" end |
.compact_tool_arg(data) ⇒ Object
83 84 85 86 87 88 89 90 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 83 def compact_tool_arg(data) key = %w[query url path file command pattern task].find { |candidate| data.key?(candidate) && !data[candidate].to_s.empty? } return nil unless key value = key == "url" ? tool_url_host(data[key].to_s) : data[key].to_s escaped = escape_tool_label(truncate_tool_label(value)) value.match?(/\A[\w.-]+\z/) ? escaped : %("#{escaped}") end |
.config_initial_selection(choices) ⇒ Object
198 199 200 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 198 def config_initial_selection(choices) choices.index { |choice| choice[:current] } || choices.index { |choice| !choice[:disabled] } || 0 end |
.config_menu_choices(current_config) ⇒ Object
175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 175 def (current_config) choices = current_config.models.each_with_index.map do |model, index| type_badge = case model["type"] when "default" then "[default] " when "lite" then "[lite] " else "" end { label: "#{type_badge}#{model["model"] || "unnamed"} (#{mask_api_key(model["api_key"])})", value: { action: :switch, model_id: model["id"] }, current: index == current_config.current_model_index } end choices + [ { label: "─" * 50, disabled: true }, { label: "[+] Add New Model", value: { action: :add } }, { label: "[*] Edit Current Model", value: { action: :edit } }, (current_config.models.length > 1 ? { label: "[-] Delete Model", value: { action: :delete } } : nil), { label: "[X] Close", value: { action: :close } } ].compact end |
.escape_tool_label(text) ⇒ Object
105 106 107 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 105 def escape_tool_label(text) text.to_s.gsub("\\", "\\\\\\").gsub('"', '\"') end |
.expand_ansi_multiline_spans(text) ⇒ Object
118 119 120 121 122 123 124 125 126 127 128 129 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 118 def (text) active = +"" text.to_s.lines.map do |line| body = line.chomp prefix = body.start_with?("\e[") || active.empty? ? "" : active body.scan(/\e\[[0-9;:]*m/).each do |code| active = code == RubyRich::AnsiCode.reset ? +"" : code end suffix = !active.empty? && !body.end_with?(RubyRich::AnsiCode.reset) ? RubyRich::AnsiCode.reset : "" "#{prefix}#{body}#{suffix}" end.join("\n") end |
.extract_thinking_and_content(content) ⇒ Object
151 152 153 154 155 156 157 158 159 160 161 162 163 164 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 151 def extract_thinking_and_content(content) return ["", content.to_s] if content.nil? thinking_parts = [] clean = content.to_s.dup clean.gsub!(%r{<think(?:ing)?>\s*([\s\S]*?)\s*</think(?:ing)?>}mi) do thinking_parts << Regexp.last_match(1).strip "" end clean = clean.gsub(/\n{3,}/, "\n\n").strip [thinking_parts.join("\n\n"), clean] end |
.format_args(args) ⇒ Object
25 26 27 28 29 30 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 25 def format_args(args) data = args.is_a?(String) ? (JSON.parse(args) rescue args) : args return data.to_s unless data.is_a?(Hash) && !data.empty? data.map { |k, v| "#{k}: #{format_tool_value(v)}" }.join("\n") end |
.format_tool_output(text, status = :done) ⇒ Object
── Tool output formatting ──────────────────────────────────
18 19 20 21 22 23 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 18 def format_tool_output(text, status = :done) marker = status == :error ? "[Error]" : "[OK]" color = status == :error ? :red : :green clean = text.to_s.sub(/\A\[(?:OK|Error)\]\s*/, "") "#{RubyRich::AnsiCode.color(color, true)}#{marker}#{RubyRich::AnsiCode.reset} #{clean}" end |
.format_tool_value(v) ⇒ Object
32 33 34 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 32 def format_tool_value(v) v.is_a?(String) ? v : JSON.generate(v) end |
.mask_api_key(api_key) ⇒ Object
── Config / dialog helpers ─────────────────────────────────
168 169 170 171 172 173 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 168 def mask_api_key(api_key) key = api_key.to_s return "not set" if key.empty? "#{key[0..5]}...#{key[-4..]}" end |
.merge_model_form_values(result, model:, default_model:, default_base_url:) ⇒ Object
202 203 204 205 206 207 208 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 202 def merge_model_form_values(result, model:, default_model:, default_base_url:) { api_key: result[:api_key].to_s.empty? ? model["api_key"] : result[:api_key], model: result[:model].to_s.empty? ? (model["model"] || default_model) : result[:model], base_url: result[:base_url].to_s.empty? ? (model["base_url"] || default_base_url) : result[:base_url] } end |
.normalize_markdown_for_terminal(text) ⇒ Object
── Markdown helpers ────────────────────────────────────────
111 112 113 114 115 116 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 111 def normalize_markdown_for_terminal(text) text.to_s .gsub(/\r\n?/, "\n") .gsub(/\A[ \t]*\n+/, "") .gsub(/\n+[ \t]*\z/, "") end |
.normalize_todo(todo) ⇒ Object
36 37 38 39 40 41 42 43 44 45 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 36 def normalize_todo(todo) case todo when Hash title = todo[:content] || todo["content"] || todo[:title] || todo["title"] || todo[:task] || todo["task"] status = todo[:status] || todo["status"] || :pending { label: title.to_s, title: title.to_s, status: status.to_sym } else { label: todo.to_s, title: todo.to_s, status: :pending } end end |
.normalize_tool_args(args) ⇒ Object
70 71 72 73 74 75 76 77 78 79 80 81 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 70 def normalize_tool_args(args) parsed = if args.is_a?(String) JSON.parse(args) else args end return {} unless parsed.is_a?(Hash) parsed.each_with_object({}) { |(key, value), hash| hash[key.to_s] = value } rescue JSON::ParserError {} end |
.parse_diff_stats(diff_text) ⇒ Object
── Diff / stats helpers ────────────────────────────────────
133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 133 def parse_diff_stats(diff_text) adds = 0 dels = 0 hunks = 0 diff_text.each_line do |line| adds += 1 if line.start_with?("+") && !line.start_with?("+++") dels += 1 if line.start_with?("-") && !line.start_with?("---") hunks += 1 if line.start_with?("@@") end return "" if adds.zero? && dels.zero? parts = [] parts << "+#{adds}" if adds.positive? parts << "-#{dels}" if dels.positive? parts << "#{hunks} hunks" if hunks.positive? " (#{parts.join(", ")})" end |
.parse_tool_info(message) ⇒ Object
── Approval helpers ────────────────────────────────────────
233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 233 def parse_tool_info() return [nil, {}] unless tool_name = [/\A\w+/]&.downcase params = {} case tool_name when "edit", "write" path = [/\((.+?)\)/, 1] params[:path] = path if path when "terminal", "shell", "exec" cmd = [/"(.+?)"/, 1] params[:command] = cmd if cmd when "web_search", "web_fetch" params[:query] = [(.index("(")&.+(1) || 0)..]&.chomp(")")&.strip when "execute", "run" params[:command] = [(.index("(")&.+(1) || 0)..]&.chomp(")")&.strip end params.reject! { |_, v| v.to_s.empty? } [tool_name, params] end |
.tool_activity_label(name, args) ⇒ Object
── Tool activity label helpers ─────────────────────────────
49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 49 def tool_activity_label(name, args) tool_name = name.to_s data = normalize_tool_args(args) case tool_name when "web_search" query = data["query"].to_s return tool_name if query.empty? %(web_search("#{escape_tool_label(truncate_tool_label(query))}")) when "web_fetch" url = data["url"].to_s return tool_name if url.empty? "web_fetch(#{truncate_tool_label(tool_url_host(url))})" else compact = compact_tool_arg(data) compact ? "#{tool_name}(#{compact})" : tool_name end end |
.tool_category(tool_name) ⇒ Object
271 272 273 274 275 276 277 278 279 280 281 282 283 284 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 271 def tool_category(tool_name) case tool_name when "read", "write", "edit", "patch", "apply_patch", "grep", "list" :file when "shell", "terminal", "exec", "execute", "run" :shell when "web_search", "web_fetch", "fetch_url" :network when "install", "billing", "payment" :paid else :file end end |
.tool_risk_level(tool_name) ⇒ Object
256 257 258 259 260 261 262 263 264 265 266 267 268 269 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 256 def tool_risk_level(tool_name) case tool_name when "read", "grep", "list", "search", "web_search", "web_fetch", "fetch_url" :low when "edit", "write", "patch", "apply_patch" :medium when "shell", "terminal", "exec", "execute", "run" :high when "install", "remove", "delete", "rm", "force" :critical else :medium end end |
.tool_url_host(url) ⇒ Object
92 93 94 95 96 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 92 def tool_url_host(url) URI.parse(url).host || url rescue URI::InvalidURIError url end |
.truncate_tool_label(text, limit = 40) ⇒ Object
98 99 100 101 102 103 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 98 def truncate_tool_label(text, limit = 40) chars = text.to_s.each_char.to_a return text.to_s if chars.length <= limit "#{chars.first(limit - 3).join}..." end |
.validate_model_form(values, is_new:, existing_model:, test_callback:) ⇒ Object
210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 |
# File 'lib/clacky/rich_ui/view_renderer.rb', line 210 def validate_model_form(values, is_new:, existing_model:, test_callback:) if is_new return { success: false, error: "API Key is required for new model" } if values[:api_key].to_s.empty? return { success: false, error: "Model name is required" } if values[:model].to_s.empty? return { success: false, error: "Base URL is required" } if values[:base_url].to_s.empty? end return { success: true } unless test_callback temp_config = Clacky::AgentConfig.new( models: [{ "api_key" => values[:api_key], "model" => values[:model], "base_url" => values[:base_url], "anthropic_format" => existing_model["anthropic_format"] }], current_model_index: 0 ) test_callback.call(temp_config) end |