Class: RailsAiContext::Tools::GetPartialInterface

Inherits:
BaseTool
  • Object
show all
Defined in:
lib/rails_ai_context/tools/get_partial_interface.rb

Constant Summary

Constants inherited from BaseTool

BaseTool::SESSION_CONTEXT, BaseTool::SHARED_CACHE

Class Method Summary collapse

Methods inherited from BaseTool

abstract!, abstract?, cache_key, cached_context, config, extract_method_source_from_file, extract_method_source_from_string, find_closest_match, fuzzy_find_key, inherited, not_found_response, paginate, rails_app, registered_tools, reset_all_caches!, reset_cache!, session_queries, session_record, session_reset!, set_call_params, text_response

Class Method Details

.call(partial:, detail: "standard", server_context: nil) ⇒ Object



28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
# File 'lib/rails_ai_context/tools/get_partial_interface.rb', line 28

def self.call(partial:, detail: "standard", server_context: nil)
  # Guard: required parameter
  if partial.nil? || partial.strip.empty?
    return text_response("The `partial` parameter is required. Provide a partial path relative to app/views (e.g. 'shared/status_badge').")
  end

  # Reject path traversal attempts
  if partial.include?("..") || partial.start_with?("/")
    return text_response("Path not allowed: #{partial}")
  end

  root = Rails.root.to_s
  views_dir = File.join(root, "app", "views")

  unless Dir.exist?(views_dir)
    return text_response("No app/views/ directory found.")
  end

  # Resolve partial to actual file path
  file_path = resolve_partial_path(views_dir, partial)

  unless file_path
    available = find_available_partials(views_dir, root)
    return not_found_response("Partial", partial, available,
      recovery_tool: "Call rails_get_view(detail:\"summary\") to see all views and partials")
  end

  # Derive display-string bases from the realpath that resolve_partial_path
  # already computed internally — keeps all path operations on realpaths.
  real_root = File.realpath(root)
  real_views_dir = File.realpath(views_dir)

  if File.size(file_path) > max_file_size
    return text_response("Partial file too large: #{file_path} (#{File.size(file_path)} bytes, max: #{max_file_size})")
  end

  source = safe_read(file_path)
  return text_response("Could not read partial file.") unless source

  relative_path = file_path.sub("#{real_root}/", "")
  partial_name = file_path.sub("#{real_views_dir}/", "")

  # Parse the partial's interface
  magic_locals = extract_magic_comment_locals(source)
  render_sites = find_render_sites(views_dir, partial, root)
  method_calls = {}

  # Primary: locals from render call sites (ground truth)
  render_locals = render_sites.flat_map { |rs| rs[:locals] || [] }.uniq

  # Secondary: local_assigns checks + defined? guards in partial source
  source_locals = extract_local_variable_references(source)

  # Combine: render-site locals first, then source-detected locals
  # Filter out noise: single chars, capitalized words, known helpers
  all_locals = (magic_locals + render_locals + source_locals).uniq
    .reject { |l| l.length <= 1 || l.match?(/\A[A-Z]/) || l.match?(/\Arender_/) }
    .sort

  # Extract method calls only for confirmed locals
  method_calls = extract_method_calls_on_locals(source, all_locals) if all_locals.any?

  case detail
  when "summary"
    format_summary(partial_name, all_locals, magic_locals, render_sites)
  when "standard"
    format_standard(partial_name, relative_path, source, all_locals, magic_locals, render_sites, method_calls)
  when "full"
    format_full(partial_name, relative_path, source, all_locals, magic_locals, render_sites, method_calls)
  else
    text_response("Unknown detail level: #{detail}. Use summary, standard, or full.")
  end
end