rubyn 0.1.0

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 (79) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/README.md +251 -0
  4. data/Rakefile +12 -0
  5. data/exe/rubyn +5 -0
  6. data/lib/generators/rubyn/install_generator.rb +16 -0
  7. data/lib/rubyn/cli.rb +85 -0
  8. data/lib/rubyn/client/api_client.rb +172 -0
  9. data/lib/rubyn/commands/agent.rb +191 -0
  10. data/lib/rubyn/commands/base.rb +60 -0
  11. data/lib/rubyn/commands/config.rb +51 -0
  12. data/lib/rubyn/commands/dashboard.rb +85 -0
  13. data/lib/rubyn/commands/index.rb +101 -0
  14. data/lib/rubyn/commands/init.rb +166 -0
  15. data/lib/rubyn/commands/refactor.rb +175 -0
  16. data/lib/rubyn/commands/review.rb +61 -0
  17. data/lib/rubyn/commands/spec.rb +72 -0
  18. data/lib/rubyn/commands/usage.rb +56 -0
  19. data/lib/rubyn/config/credentials.rb +39 -0
  20. data/lib/rubyn/config/project_config.rb +42 -0
  21. data/lib/rubyn/config/settings.rb +53 -0
  22. data/lib/rubyn/context/codebase_indexer.rb +195 -0
  23. data/lib/rubyn/context/context_builder.rb +36 -0
  24. data/lib/rubyn/context/file_resolver.rb +235 -0
  25. data/lib/rubyn/context/project_scanner.rb +132 -0
  26. data/lib/rubyn/engine/app/assets/images/rubyn/RubynLogo.png +0 -0
  27. data/lib/rubyn/engine/app/assets/javascripts/rubyn/application.js +579 -0
  28. data/lib/rubyn/engine/app/assets/stylesheets/rubyn/application.css +1073 -0
  29. data/lib/rubyn/engine/app/controllers/rubyn/agent_controller.rb +26 -0
  30. data/lib/rubyn/engine/app/controllers/rubyn/application_controller.rb +44 -0
  31. data/lib/rubyn/engine/app/controllers/rubyn/dashboard_controller.rb +19 -0
  32. data/lib/rubyn/engine/app/controllers/rubyn/feedback_controller.rb +17 -0
  33. data/lib/rubyn/engine/app/controllers/rubyn/files_controller.rb +54 -0
  34. data/lib/rubyn/engine/app/controllers/rubyn/refactor_controller.rb +56 -0
  35. data/lib/rubyn/engine/app/controllers/rubyn/reviews_controller.rb +32 -0
  36. data/lib/rubyn/engine/app/controllers/rubyn/settings_controller.rb +17 -0
  37. data/lib/rubyn/engine/app/controllers/rubyn/specs_controller.rb +33 -0
  38. data/lib/rubyn/engine/app/views/layouts/rubyn/application.html.erb +63 -0
  39. data/lib/rubyn/engine/app/views/rubyn/agent/show.html.erb +22 -0
  40. data/lib/rubyn/engine/app/views/rubyn/dashboard/index.html.erb +120 -0
  41. data/lib/rubyn/engine/app/views/rubyn/files/index.html.erb +45 -0
  42. data/lib/rubyn/engine/app/views/rubyn/refactor/show.html.erb +28 -0
  43. data/lib/rubyn/engine/app/views/rubyn/reviews/show.html.erb +28 -0
  44. data/lib/rubyn/engine/app/views/rubyn/settings/show.html.erb +42 -0
  45. data/lib/rubyn/engine/app/views/rubyn/specs/show.html.erb +28 -0
  46. data/lib/rubyn/engine/config/routes.rb +13 -0
  47. data/lib/rubyn/engine/engine.rb +18 -0
  48. data/lib/rubyn/output/diff_renderer.rb +106 -0
  49. data/lib/rubyn/output/formatter.rb +123 -0
  50. data/lib/rubyn/output/spinner.rb +26 -0
  51. data/lib/rubyn/tools/base_tool.rb +74 -0
  52. data/lib/rubyn/tools/bundle_add.rb +77 -0
  53. data/lib/rubyn/tools/create_file.rb +32 -0
  54. data/lib/rubyn/tools/delete_file.rb +29 -0
  55. data/lib/rubyn/tools/executor.rb +68 -0
  56. data/lib/rubyn/tools/find_files.rb +33 -0
  57. data/lib/rubyn/tools/find_references.rb +72 -0
  58. data/lib/rubyn/tools/git_commit.rb +65 -0
  59. data/lib/rubyn/tools/git_create_branch.rb +58 -0
  60. data/lib/rubyn/tools/git_diff.rb +42 -0
  61. data/lib/rubyn/tools/git_log.rb +43 -0
  62. data/lib/rubyn/tools/git_status.rb +26 -0
  63. data/lib/rubyn/tools/list_directory.rb +82 -0
  64. data/lib/rubyn/tools/move_file.rb +35 -0
  65. data/lib/rubyn/tools/patch_file.rb +47 -0
  66. data/lib/rubyn/tools/rails_generate.rb +40 -0
  67. data/lib/rubyn/tools/rails_migrate.rb +55 -0
  68. data/lib/rubyn/tools/rails_routes.rb +35 -0
  69. data/lib/rubyn/tools/read_file.rb +45 -0
  70. data/lib/rubyn/tools/registry.rb +28 -0
  71. data/lib/rubyn/tools/run_command.rb +48 -0
  72. data/lib/rubyn/tools/run_tests.rb +52 -0
  73. data/lib/rubyn/tools/search_files.rb +82 -0
  74. data/lib/rubyn/tools/write_file.rb +30 -0
  75. data/lib/rubyn/version.rb +5 -0
  76. data/lib/rubyn/version_checker.rb +74 -0
  77. data/lib/rubyn.rb +95 -0
  78. data/sig/rubyn.rbs +4 -0
  79. metadata +379 -0
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyn
4
+ class AgentController < ApplicationController
5
+ def show
6
+ @conversations = begin
7
+ api_client.list_conversations(project_token: project_token)
8
+ rescue Rubyn::Error
9
+ []
10
+ end
11
+ end
12
+
13
+ def create
14
+ result = api_client.agent_message(
15
+ conversation_id: params[:conversation_id]&.to_i,
16
+ message: params[:message],
17
+ file_context: [],
18
+ project_token: project_token
19
+ )
20
+
21
+ render json: result
22
+ rescue Rubyn::Error => e
23
+ render json: { error: e.message }, status: :unprocessable_entity
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyn
4
+ class ApplicationController < ActionController::Base
5
+ layout "rubyn/application"
6
+
7
+ protect_from_forgery with: :null_session
8
+
9
+ before_action :check_for_gem_update
10
+
11
+ private
12
+
13
+ def check_for_gem_update
14
+ @update_message = Rubyn::VersionChecker.update_message
15
+ rescue Rubyn::Error, Net::HTTPError, SocketError, Timeout::Error
16
+ @update_message = nil
17
+ end
18
+
19
+ def api_client
20
+ @api_client ||= Rubyn.api_client
21
+ end
22
+
23
+ def project_config
24
+ @project_config ||= Config::ProjectConfig.read
25
+ end
26
+
27
+ def project_id
28
+ project_config["project_id"]
29
+ end
30
+
31
+ def project_token
32
+ project_config["project_token"]
33
+ end
34
+
35
+ def build_context_files(file_path)
36
+ resolver = Context::FileResolver.new(Rails.root.to_s)
37
+ related = resolver.resolve(file_path)
38
+ builder = Context::ContextBuilder.new(Rails.root.to_s)
39
+ context = builder.build(file_path, related)
40
+ context.reject { |path, _| path == file_path }
41
+ .map { |path, content| { file_path: path, code: content } }
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyn
4
+ class DashboardController < ApplicationController
5
+ def index
6
+ @balance = begin
7
+ api_client.get_balance
8
+ rescue Rubyn::Error
9
+ {}
10
+ end
11
+ @history = begin
12
+ api_client.get_history(page: 1)
13
+ rescue Rubyn::Error
14
+ {}
15
+ end
16
+ @project = project_config
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyn
4
+ class FeedbackController < ApplicationController
5
+ def create
6
+ result = api_client.submit_feedback(
7
+ interaction_id: params[:interaction_id].to_i,
8
+ rating: params[:rating],
9
+ feedback: params[:feedback]
10
+ )
11
+
12
+ render json: result
13
+ rescue Rubyn::Error => e
14
+ render json: { error: e.message }, status: :unprocessable_entity
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyn
4
+ class FilesController < ApplicationController
5
+ CATEGORIES = {
6
+ "Models" => "app/models/**/*.rb",
7
+ "Controllers" => "app/controllers/**/*.rb",
8
+ "Views" => "app/views/**/*.{erb,haml,slim}",
9
+ "Helpers" => "app/helpers/**/*.rb",
10
+ "Services" => "app/services/**/*.rb",
11
+ "Query Objects" => "app/queries/**/*.rb",
12
+ "Jobs" => "app/jobs/**/*.rb",
13
+ "Mailers" => "app/mailers/**/*.rb",
14
+ "Channels" => "app/channels/**/*.rb",
15
+ "Serializers" => "app/serializers/**/*.rb",
16
+ "Policies" => "app/policies/**/*.rb",
17
+ "Lib" => "lib/**/*.rb",
18
+ "Config" => "config/**/*.rb",
19
+ "Specs" => "spec/**/*.rb",
20
+ "Tests" => "test/**/*.rb"
21
+ }.freeze
22
+
23
+ def index
24
+ all_files = scan_ruby_files
25
+ @categories = categorize(all_files)
26
+ @total_count = all_files.size
27
+ end
28
+
29
+ private
30
+
31
+ def scan_ruby_files
32
+ Dir.glob(File.join(Rails.root, "**/*.{rb,erb,haml,slim}"))
33
+ .reject { |f| f.include?("/vendor/") || f.include?("/node_modules/") }
34
+ .map { |f| f.sub("#{Rails.root}/", "") }
35
+ .sort
36
+ end
37
+
38
+ def categorize(files)
39
+ categorized = {}
40
+ remaining = files.dup
41
+
42
+ CATEGORIES.each do |label, pattern|
43
+ matched = remaining.select { |f| File.fnmatch?(pattern, f, File::FNM_PATHNAME) }
44
+ next if matched.empty?
45
+
46
+ categorized[label] = matched
47
+ remaining -= matched
48
+ end
49
+
50
+ categorized["Other"] = remaining if remaining.any?
51
+ categorized
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyn
4
+ class RefactorController < ApplicationController
5
+ def show
6
+ @file = params[:file]
7
+ end
8
+
9
+ def create
10
+ file_path = params[:file]
11
+ full_path = Rails.root.join(file_path)
12
+
13
+ unless File.exist?(full_path)
14
+ render json: { error: "File not found: #{file_path}" }, status: :not_found
15
+ return
16
+ end
17
+
18
+ code = File.read(full_path)
19
+ context_files = build_context_files(file_path)
20
+
21
+ result = api_client.refactor(
22
+ file_path: file_path,
23
+ code: code,
24
+ context_files: context_files,
25
+ project_token: project_token
26
+ )
27
+
28
+ render json: result
29
+ rescue Rubyn::Error => e
30
+ render json: { error: e.message }, status: :unprocessable_entity
31
+ end
32
+
33
+ def update
34
+ file_path = params[:file]
35
+ code = params[:code]
36
+
37
+ unless file_path.present? && code.present?
38
+ render json: { error: "Missing file path or code" }, status: :bad_request
39
+ return
40
+ end
41
+
42
+ full_path = Rails.root.join(file_path)
43
+
44
+ # Create directories for new files (e.g., app/services/orders/)
45
+ FileUtils.mkdir_p(File.dirname(full_path))
46
+
47
+ created = !File.exist?(full_path)
48
+ File.write(full_path, code)
49
+
50
+ action = created ? "Created" : "Updated"
51
+ render json: { success: true, message: "#{action} #{file_path}" }
52
+ rescue Errno::EACCES, Errno::ENOENT, Errno::ENOSPC => e
53
+ render json: { error: e.message }, status: :unprocessable_entity
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,32 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyn
4
+ class ReviewsController < ApplicationController
5
+ def show
6
+ @file = params[:file]
7
+ end
8
+
9
+ def create
10
+ file_path = params[:file]
11
+ full_path = Rails.root.join(file_path)
12
+
13
+ unless File.exist?(full_path)
14
+ render json: { error: "File not found: #{file_path}" }, status: :not_found
15
+ return
16
+ end
17
+
18
+ code = File.read(full_path)
19
+ context_files = build_context_files(file_path)
20
+
21
+ result = api_client.review(
22
+ files: [{ file_path: file_path, code: code }],
23
+ context_files: context_files,
24
+ project_token: project_token
25
+ )
26
+
27
+ render json: result
28
+ rescue Rubyn::Error => e
29
+ render json: { error: e.message }, status: :unprocessable_entity
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyn
4
+ class SettingsController < ApplicationController
5
+ def show
6
+ @settings = Config::Settings.all
7
+ @credentials_exist = Config::Credentials.exists?
8
+ end
9
+
10
+ def update
11
+ params.permit(:default_model, :auto_apply, :output_format).each do |key, value|
12
+ Config::Settings.set(key, value)
13
+ end
14
+ redirect_to rubyn.settings_path, notice: "Settings updated."
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rubyn
4
+ class SpecsController < ApplicationController
5
+ def show
6
+ @file = params[:file]
7
+ end
8
+
9
+ def create
10
+ file_path = params[:file]
11
+ full_path = Rails.root.join(file_path)
12
+
13
+ unless File.exist?(full_path)
14
+ render json: { error: "File not found: #{file_path}" }, status: :not_found
15
+ return
16
+ end
17
+
18
+ code = File.read(full_path)
19
+ context_files = build_context_files(file_path)
20
+
21
+ result = api_client.generate_spec(
22
+ file_path: file_path,
23
+ code: code,
24
+ context_files: context_files,
25
+ project_token: project_token
26
+ )
27
+
28
+ render json: result
29
+ rescue Rubyn::Error => e
30
+ render json: { error: e.message }, status: :unprocessable_entity
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,63 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Rubyn - <%= yield(:title) || "Dashboard" %></title>
7
+ <%= stylesheet_link_tag "rubyn/application", media: "all" %>
8
+ <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/styles/atom-one-dark.min.css" />
9
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
10
+ <script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/languages/ruby.min.js"></script>
11
+ <%= javascript_include_tag "rubyn/application" %>
12
+ <%= csrf_meta_tags %>
13
+ </head>
14
+ <body>
15
+ <div class="rubyn-shell">
16
+ <aside class="rubyn-sidebar">
17
+ <div class="rubyn-sidebar-brand">
18
+ <%= link_to rubyn.root_path do %>
19
+ <%= image_tag "rubyn/RubynLogo.png", class: "rubyn-logo-img", alt: "Rubyn" %> Rubyn
20
+ <% end %>
21
+ </div>
22
+
23
+ <nav class="rubyn-sidebar-nav">
24
+ <%= link_to rubyn.root_path, class: ("active" if current_page?(rubyn.root_path)) do %>
25
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><rect x="3" y="3" width="7" height="7" rx="1"/><rect x="14" y="3" width="7" height="7" rx="1"/><rect x="3" y="14" width="7" height="7" rx="1"/><rect x="14" y="14" width="7" height="7" rx="1"/></svg>
26
+ Dashboard
27
+ <% end %>
28
+ <%= link_to rubyn.files_path, class: ("active" if current_page?(rubyn.files_path)) do %>
29
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
30
+ Files
31
+ <% end %>
32
+ <%= link_to rubyn.agent_path, class: ("active" if current_page?(rubyn.agent_path)) do %>
33
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
34
+ Agent
35
+ <% end %>
36
+ <%= link_to rubyn.settings_path, class: ("active" if current_page?(rubyn.settings_path)) do %>
37
+ <svg class="nav-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"/></svg>
38
+ Settings
39
+ <% end %>
40
+ </nav>
41
+
42
+ <div class="rubyn-sidebar-footer">
43
+ Rubyn v<%= Rubyn::VERSION %>
44
+ </div>
45
+ </aside>
46
+
47
+ <div class="rubyn-content">
48
+ <main class="rubyn-main">
49
+ <% if @update_message %>
50
+ <div class="rubyn-update-banner">
51
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>
52
+ <%= @update_message %>
53
+ </div>
54
+ <% end %>
55
+ <% if notice %>
56
+ <div class="rubyn-notice"><%= notice %></div>
57
+ <% end %>
58
+ <%= yield %>
59
+ </main>
60
+ </div>
61
+ </div>
62
+ </body>
63
+ </html>
@@ -0,0 +1,22 @@
1
+ <% content_for(:title) { "Agent" } %>
2
+
3
+ <div class="rubyn-page-header">
4
+ <h1>Agent</h1>
5
+ <p>Ask anything about your codebase</p>
6
+ </div>
7
+
8
+ <div class="rubyn-agent-container">
9
+ <div class="rubyn-chat" id="chat-container">
10
+ <div class="rubyn-messages" id="messages">
11
+ <div class="rubyn-empty-chat" id="empty-state">
12
+ <svg class="rubyn-empty-chat-icon" width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.3"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
13
+ <p>Start a conversation with Rubyn</p>
14
+ </div>
15
+ </div>
16
+
17
+ <form class="rubyn-chat-input" id="chat-form">
18
+ <textarea id="message-input" placeholder="Ask Rubyn anything about your code..." rows="2"></textarea>
19
+ <button type="submit" class="rubyn-btn">Send</button>
20
+ </form>
21
+ </div>
22
+ </div>
@@ -0,0 +1,120 @@
1
+ <% content_for(:title) { "Dashboard" } %>
2
+
3
+ <div class="rubyn-page-header">
4
+ <h1>Dashboard</h1>
5
+ <p>Your project at a glance</p>
6
+ </div>
7
+
8
+ <div class="rubyn-cards">
9
+ <div class="rubyn-card rubyn-card--credits">
10
+ <h3>Credits</h3>
11
+ <p class="rubyn-stat primary"><%= @balance["balance"] rescue "N/A" %></p>
12
+ <% if @balance["credit_allowance"] %>
13
+ <p class="rubyn-label">of <%= @balance["credit_allowance"] %> · resets <%= @balance["reset_period"] %></p>
14
+ <% else %>
15
+ <p class="rubyn-label">remaining</p>
16
+ <% end %>
17
+ </div>
18
+
19
+ <div class="rubyn-card rubyn-card--plan">
20
+ <h3>Plan</h3>
21
+ <p class="rubyn-stat success"><%= @balance["plan"]&.capitalize rescue "N/A" %></p>
22
+ <% if @balance["resets_at"] %>
23
+ <p class="rubyn-label">resets <%= Date.parse(@balance["resets_at"]).strftime("%b %-d") rescue @balance["resets_at"] %></p>
24
+ <% end %>
25
+ </div>
26
+
27
+ <div class="rubyn-card rubyn-card--project">
28
+ <h3>Project</h3>
29
+ <p class="rubyn-stat"><%= @project["project_id"] rescue "Not configured" %></p>
30
+ </div>
31
+ </div>
32
+
33
+ <% interactions = @history["interactions"] || [] %>
34
+ <% if interactions.any? %>
35
+ <div class="rubyn-usage-section">
36
+ <h2>Recent Usage</h2>
37
+ <table class="rubyn-usage-table">
38
+ <thead>
39
+ <tr>
40
+ <th>Action</th>
41
+ <th>Credits</th>
42
+ <th>Time</th>
43
+ </tr>
44
+ </thead>
45
+ <tbody>
46
+ <% interactions.first(10).each do |i| %>
47
+ <tr>
48
+ <td><span class="rubyn-usage-command"><%= i["command"] %></span></td>
49
+ <td><%= i["credits_charged"] %></td>
50
+ <td class="rubyn-text-dim"><%= Time.parse(i["created_at"]).strftime("%b %-d, %-I:%M%P") rescue i["created_at"] %></td>
51
+ </tr>
52
+ <% end %>
53
+ </tbody>
54
+ </table>
55
+ </div>
56
+ <% end %>
57
+
58
+ <div class="rubyn-actions">
59
+ <h2>Quick Actions</h2>
60
+ <div class="rubyn-actions-grid">
61
+ <%= link_to rubyn.files_path, class: "rubyn-action-card" do %>
62
+ <span class="rubyn-action-icon rubyn-action-icon--files">
63
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M13 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9z"/><polyline points="13 2 13 9 20 9"/></svg>
64
+ </span>
65
+ Browse Files
66
+ <% end %>
67
+ <%= link_to rubyn.agent_path, class: "rubyn-action-card" do %>
68
+ <span class="rubyn-action-icon rubyn-action-icon--agent">
69
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/></svg>
70
+ </span>
71
+ Start Agent
72
+ <% end %>
73
+ <%= link_to rubyn.settings_path, class: "rubyn-action-card" do %>
74
+ <span class="rubyn-action-icon rubyn-action-icon--settings">
75
+ <svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9"/></svg>
76
+ </span>
77
+ Settings
78
+ <% end %>
79
+ </div>
80
+ </div>
81
+
82
+ <div class="rubyn-actions" id="recent-results" style="display:none;">
83
+ <h2>Pending Results</h2>
84
+ <p class="rubyn-label" style="margin-bottom: 1rem;">You have unsaved refactoring results. Click to review and apply.</p>
85
+ <div class="rubyn-actions-grid" id="recent-results-grid"></div>
86
+ </div>
87
+
88
+ <script>
89
+ document.addEventListener("DOMContentLoaded", function() {
90
+ var grid = document.getElementById("recent-results-grid");
91
+ var section = document.getElementById("recent-results");
92
+ var found = 0;
93
+
94
+ for (var i = 0; i < sessionStorage.length; i++) {
95
+ var key = sessionStorage.key(i);
96
+ if (!key.startsWith("rubyn:")) continue;
97
+
98
+ var parts = key.split(":");
99
+ var resource = parts[1];
100
+ var file = parts.slice(2).join(":");
101
+
102
+ var label = resource.charAt(0).toUpperCase() + resource.slice(1);
103
+ var mountPath = "<%= rubyn.root_path.chomp('/') %>";
104
+ var href = mountPath + "/" + resource + "?file=" + encodeURIComponent(file);
105
+
106
+ var card = document.createElement("a");
107
+ card.href = href;
108
+ card.className = "rubyn-action-card";
109
+ card.innerHTML = '<span class="rubyn-action-icon rubyn-action-icon--files">' +
110
+ '<svg width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>' +
111
+ '</span>' +
112
+ '<div><strong>' + label + '</strong><br><span style="font-size:0.75rem;opacity:0.7;font-family:monospace;">' + file + '</span></div>';
113
+
114
+ grid.appendChild(card);
115
+ found++;
116
+ }
117
+
118
+ if (found > 0) section.style.display = "block";
119
+ });
120
+ </script>
@@ -0,0 +1,45 @@
1
+ <% content_for(:title) { "Files" } %>
2
+
3
+ <div class="rubyn-page-header">
4
+ <h1>Project Files</h1>
5
+ <p><%= @total_count %> files found across <%= @categories.size %> categories</p>
6
+ </div>
7
+
8
+ <% @categories.each do |category, files| %>
9
+ <div class="rubyn-file-category">
10
+ <button class="rubyn-file-category-header" onclick="toggleCategory(this)">
11
+ <span class="rubyn-file-category-title">
12
+ <svg class="rubyn-file-category-chevron" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="6 9 12 15 18 9"/></svg>
13
+ <%= category %>
14
+ </span>
15
+ <span class="rubyn-file-category-count"><%= files.size %></span>
16
+ </button>
17
+
18
+ <div class="rubyn-file-list">
19
+ <% files.each do |file| %>
20
+ <div class="rubyn-file-item">
21
+ <span class="rubyn-file-name">
22
+ <svg class="rubyn-file-icon" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
23
+ <%= file %>
24
+ </span>
25
+ <% if file.end_with?(".rb") %>
26
+ <div class="rubyn-file-actions">
27
+ <%= link_to "Refactor", rubyn.refactor_path(file: file), class: "rubyn-btn-sm", target: "_blank" %>
28
+ <%= link_to "Spec", rubyn.specs_path(file: file), class: "rubyn-btn-sm", target: "_blank" %>
29
+ <%= link_to "Review", rubyn.reviews_path(file: file), class: "rubyn-btn-sm", target: "_blank" %>
30
+ </div>
31
+ <% end %>
32
+ </div>
33
+ <% end %>
34
+ </div>
35
+ </div>
36
+ <% end %>
37
+
38
+ <script>
39
+ function toggleCategory(button) {
40
+ var list = button.nextElementSibling;
41
+ var chevron = button.querySelector(".rubyn-file-category-chevron");
42
+ list.classList.toggle("rubyn-file-list--collapsed");
43
+ chevron.classList.toggle("rubyn-file-category-chevron--collapsed");
44
+ }
45
+ </script>
@@ -0,0 +1,28 @@
1
+ <% content_for(:title) { "Refactor" } %>
2
+
3
+ <div class="rubyn-page-header">
4
+ <h1>Refactor</h1>
5
+ <p>AI-powered refactoring suggestions</p>
6
+ </div>
7
+
8
+ <% if @file %>
9
+ <div class="rubyn-tool-container">
10
+ <div class="rubyn-tool-header">
11
+ <span class="rubyn-tool-filepath">
12
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
13
+ <%= @file %>
14
+ </span>
15
+ </div>
16
+ <div class="rubyn-tool-body" id="diff-container">
17
+ <div class="rubyn-loading">
18
+ <span class="rubyn-spinner"></span>
19
+ Generating refactoring suggestions<span class="rubyn-dots"><span></span><span></span><span></span></span>
20
+ </div>
21
+ </div>
22
+ </div>
23
+ <% else %>
24
+ <div class="rubyn-tool-empty">
25
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.3"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
26
+ <p>Select a file from the <%= link_to "file browser", rubyn.files_path %>.</p>
27
+ </div>
28
+ <% end %>
@@ -0,0 +1,28 @@
1
+ <% content_for(:title) { "Review" } %>
2
+
3
+ <div class="rubyn-page-header">
4
+ <h1>Code Review</h1>
5
+ <p>AI-powered code analysis</p>
6
+ </div>
7
+
8
+ <% if @file %>
9
+ <div class="rubyn-tool-container">
10
+ <div class="rubyn-tool-header">
11
+ <span class="rubyn-tool-filepath">
12
+ <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M14.5 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V7.5L14.5 2z"/><polyline points="14 2 14 8 20 8"/></svg>
13
+ <%= @file %>
14
+ </span>
15
+ </div>
16
+ <div class="rubyn-tool-body" id="review-container">
17
+ <div class="rubyn-loading">
18
+ <span class="rubyn-spinner"></span>
19
+ Reviewing code<span class="rubyn-dots"><span></span><span></span><span></span></span>
20
+ </div>
21
+ </div>
22
+ </div>
23
+ <% else %>
24
+ <div class="rubyn-tool-empty">
25
+ <svg width="48" height="48" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" opacity="0.3"><circle cx="11" cy="11" r="8"/><line x1="21" y1="21" x2="16.65" y2="16.65"/></svg>
26
+ <p>Select a file from the <%= link_to "file browser", rubyn.files_path %>.</p>
27
+ </div>
28
+ <% end %>
@@ -0,0 +1,42 @@
1
+ <% content_for(:title) { "Settings" } %>
2
+
3
+ <div class="rubyn-page-header">
4
+ <h1>Settings</h1>
5
+ <p>Configure your Rubyn installation</p>
6
+ </div>
7
+
8
+ <div class="rubyn-settings-grid">
9
+ <div class="rubyn-setting-card">
10
+ <h3>API Key</h3>
11
+ <p>Authentication status for the Rubyn API</p>
12
+ <% if @credentials_exist %>
13
+ <span class="rubyn-status-badge rubyn-status-badge--ok">
14
+ <span class="rubyn-status-dot"></span> Configured
15
+ </span>
16
+ <% else %>
17
+ <span class="rubyn-status-badge rubyn-status-badge--warn">
18
+ <span class="rubyn-status-dot"></span> Not configured
19
+ </span>
20
+ <% end %>
21
+ </div>
22
+
23
+ <div class="rubyn-setting-card">
24
+ <h3>Preferences</h3>
25
+
26
+ <%= form_tag rubyn.settings_path, method: :patch do %>
27
+ <div class="rubyn-form-group">
28
+ <label>Output Format</label>
29
+ <%= select_tag :output_format, options_for_select(["diff", "full"], @settings["output_format"]) %>
30
+ </div>
31
+
32
+ <div class="rubyn-form-group">
33
+ <label>
34
+ <%= check_box_tag :auto_apply, "true", @settings["auto_apply"] %>
35
+ Auto-apply changes
36
+ </label>
37
+ </div>
38
+
39
+ <%= submit_tag "Save", class: "rubyn-btn" %>
40
+ <% end %>
41
+ </div>
42
+ </div>