ruby_llm-agents 0.3.3 → 0.3.5

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 (97) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +132 -1263
  3. data/app/controllers/concerns/ruby_llm/agents/filterable.rb +5 -1
  4. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +2 -1
  5. data/app/controllers/ruby_llm/agents/agents_controller.rb +21 -2
  6. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +137 -9
  7. data/app/controllers/ruby_llm/agents/executions_controller.rb +83 -5
  8. data/app/models/ruby_llm/agents/execution/analytics.rb +103 -12
  9. data/app/models/ruby_llm/agents/execution/scopes.rb +25 -0
  10. data/app/models/ruby_llm/agents/execution/workflow.rb +299 -0
  11. data/app/models/ruby_llm/agents/execution.rb +28 -59
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +165 -0
  13. data/app/services/ruby_llm/agents/agent_registry.rb +118 -7
  14. data/app/views/layouts/ruby_llm/agents/application.html.erb +430 -0
  15. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +23 -0
  16. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +125 -0
  17. data/app/views/ruby_llm/agents/agents/index.html.erb +93 -0
  18. data/app/views/ruby_llm/agents/agents/show.html.erb +775 -0
  19. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +112 -0
  20. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +75 -0
  21. data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_execution_item.html.erb +7 -4
  22. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +84 -0
  23. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +115 -0
  24. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +49 -0
  25. data/app/views/ruby_llm/agents/dashboard/index.html.erb +155 -0
  26. data/app/views/{rubyllm → ruby_llm}/agents/executions/_execution.html.erb +1 -1
  27. data/app/views/{rubyllm → ruby_llm}/agents/executions/_filters.html.erb +39 -11
  28. data/app/views/{rubyllm → ruby_llm}/agents/executions/_list.html.erb +19 -9
  29. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +101 -0
  30. data/app/views/ruby_llm/agents/executions/index.html.erb +88 -0
  31. data/app/views/{rubyllm → ruby_llm}/agents/executions/show.html.erb +260 -200
  32. data/app/views/{rubyllm → ruby_llm}/agents/settings/show.html.erb +1 -1
  33. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +48 -0
  34. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +251 -0
  35. data/app/views/{rubyllm → ruby_llm}/agents/shared/_filter_dropdown.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +27 -0
  37. data/app/views/{rubyllm → ruby_llm}/agents/shared/_select_dropdown.html.erb +1 -1
  38. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_badge.html.erb +1 -1
  39. data/app/views/{rubyllm → ruby_llm}/agents/shared/_status_dot.html.erb +1 -1
  40. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +26 -0
  41. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +61 -0
  42. data/config/routes.rb +2 -0
  43. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +97 -0
  44. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +3 -3
  45. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +23 -0
  46. data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +2 -2
  47. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +38 -0
  48. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +45 -0
  49. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +17 -5
  50. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
  51. data/lib/ruby_llm/agents/alert_manager.rb +20 -16
  52. data/lib/ruby_llm/agents/base/caching.rb +40 -0
  53. data/lib/ruby_llm/agents/base/cost_calculation.rb +105 -0
  54. data/lib/ruby_llm/agents/base/dsl.rb +261 -0
  55. data/lib/ruby_llm/agents/base/execution.rb +258 -0
  56. data/lib/ruby_llm/agents/base/reliability_execution.rb +136 -0
  57. data/lib/ruby_llm/agents/base/response_building.rb +86 -0
  58. data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
  59. data/lib/ruby_llm/agents/base.rb +37 -801
  60. data/lib/ruby_llm/agents/budget_tracker.rb +250 -139
  61. data/lib/ruby_llm/agents/cache_helper.rb +98 -0
  62. data/lib/ruby_llm/agents/circuit_breaker.rb +48 -30
  63. data/lib/ruby_llm/agents/configuration.rb +40 -1
  64. data/lib/ruby_llm/agents/engine.rb +65 -1
  65. data/lib/ruby_llm/agents/inflections.rb +14 -0
  66. data/lib/ruby_llm/agents/instrumentation.rb +66 -0
  67. data/lib/ruby_llm/agents/reliability.rb +8 -2
  68. data/lib/ruby_llm/agents/version.rb +1 -1
  69. data/lib/ruby_llm/agents/workflow/instrumentation.rb +254 -0
  70. data/lib/ruby_llm/agents/workflow/parallel.rb +282 -0
  71. data/lib/ruby_llm/agents/workflow/pipeline.rb +306 -0
  72. data/lib/ruby_llm/agents/workflow/result.rb +390 -0
  73. data/lib/ruby_llm/agents/workflow/router.rb +429 -0
  74. data/lib/ruby_llm/agents/workflow.rb +232 -0
  75. data/lib/ruby_llm/agents.rb +1 -0
  76. metadata +57 -75
  77. data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
  78. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
  79. data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
  80. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
  81. data/app/views/layouts/rubyllm/agents/application.html.erb +0 -626
  82. data/app/views/rubyllm/agents/agents/index.html.erb +0 -20
  83. data/app/views/rubyllm/agents/agents/show.html.erb +0 -772
  84. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -165
  85. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +0 -10
  86. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
  87. data/app/views/rubyllm/agents/dashboard/index.html.erb +0 -197
  88. data/app/views/rubyllm/agents/executions/index.html.erb +0 -28
  89. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +0 -18
  90. data/app/views/rubyllm/agents/shared/_executions_table.html.erb +0 -193
  91. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_agent.html.erb +0 -0
  92. /data/app/views/{rubyllm → ruby_llm}/agents/agents/_version_comparison.html.erb +0 -0
  93. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_action_center.html.erb +0 -0
  94. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_alerts_feed.html.erb +0 -0
  95. /data/app/views/{rubyllm → ruby_llm}/agents/dashboard/_breaker_strip.html.erb +0 -0
  96. /data/app/views/{rubyllm → ruby_llm}/agents/executions/dry_run.html.erb +0 -0
  97. /data/app/views/{rubyllm → ruby_llm}/agents/shared/_stat_card.html.erb +0 -0
@@ -1,24 +1,28 @@
1
1
  <div id="execution-detail" data-execution-id="<%= @execution.id %>" data-status="<%= @execution.status %>">
2
- <div class="mb-6">
3
- <%= link_to ruby_llm_agents.executions_path, class: "inline-flex items-center text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" do %>
4
- <svg
5
- class="w-4 h-4 mr-1"
6
- fill="none"
7
- stroke="currentColor"
8
- viewBox="0 0 24 24"
9
- >
10
- <path
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- stroke-width="2"
14
- d="M10 19l-7-7m0 0l7-7m-7 7h18"
15
- />
16
- </svg>
17
- Back to Executions
18
- <% end %>
19
- </div>
2
+ <%= render "ruby_llm/agents/shared/breadcrumbs", items: [
3
+ { label: "Dashboard", path: ruby_llm_agents.root_path },
4
+ { label: "Executions", path: ruby_llm_agents.executions_path },
5
+ { label: "##{@execution.id}" }
6
+ ] %>
20
7
 
21
8
  <!-- Header -->
9
+ <%
10
+ # Collect secondary badges
11
+ secondary_badges = []
12
+ secondary_badges << { label: "Stream", color: "cyan" } if @execution.streaming?
13
+ secondary_badges << { label: "Cached", color: "purple" } if @execution.cache_hit
14
+ if @execution.finish_reason.present?
15
+ finish_color = case @execution.finish_reason
16
+ when 'stop' then 'green'
17
+ when 'length' then 'yellow'
18
+ when 'content_filter' then 'red'
19
+ when 'tool_calls' then 'blue'
20
+ else 'gray'
21
+ end
22
+ secondary_badges << { label: @execution.finish_reason, color: finish_color }
23
+ end
24
+ secondary_badges << { label: "Rate Limited", color: "orange" } if @execution.respond_to?(:rate_limited?) && @execution.rate_limited?
25
+ %>
22
26
  <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 sm:p-5 mb-6">
23
27
  <!-- Desktop: Row 1 - Agent name + badges + buttons + date -->
24
28
  <div class="hidden sm:flex sm:items-center sm:justify-between gap-4">
@@ -26,27 +30,50 @@
26
30
  <h2 class="text-lg font-bold text-gray-900 dark:text-gray-100 truncate">
27
31
  <%= @execution.agent_type.gsub(/Agent$/, '') %>
28
32
  </h2>
29
- <%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
30
- <% if @execution.streaming? %>
31
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300">Stream</span>
32
- <% end %>
33
- <% if @execution.cache_hit %>
34
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300">Cached</span>
35
- <% end %>
36
- <% if @execution.finish_reason.present? %>
37
- <% finish_colors = {
38
- 'stop' => 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300',
39
- 'length' => 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300',
40
- 'content_filter' => 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300',
41
- 'tool_calls' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300'
42
- } %>
43
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium <%= finish_colors[@execution.finish_reason] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' %>"><%= @execution.finish_reason %></span>
33
+ <%= render "ruby_llm/agents/shared/status_badge", status: @execution.status, size: :md %>
34
+ <% if secondary_badges.any? %>
35
+ <div class="relative" x-data="{ showDetails: false }">
36
+ <button
37
+ type="button"
38
+ @mouseenter="showDetails = true"
39
+ @mouseleave="showDetails = false"
40
+ class="inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
41
+ >
42
+ +<%= secondary_badges.size %>
43
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
44
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
45
+ </svg>
46
+ </button>
47
+ <div
48
+ x-show="showDetails"
49
+ x-cloak
50
+ x-transition:enter="transition ease-out duration-100"
51
+ x-transition:enter-start="opacity-0 scale-95"
52
+ x-transition:enter-end="opacity-100 scale-100"
53
+ class="absolute left-0 mt-1 z-10 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 p-2 min-w-max"
54
+ >
55
+ <div class="flex flex-wrap gap-1.5">
56
+ <% secondary_badges.each do |badge| %>
57
+ <% badge_classes = case badge[:color]
58
+ when 'cyan' then 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300'
59
+ when 'purple' then 'bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300'
60
+ when 'green' then 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300'
61
+ when 'yellow' then 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300'
62
+ when 'red' then 'bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-300'
63
+ when 'blue' then 'bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300'
64
+ when 'orange' then 'bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-300'
65
+ else 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300'
66
+ end %>
67
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium <%= badge_classes %>"><%= badge[:label] %></span>
68
+ <% end %>
69
+ </div>
70
+ </div>
71
+ </div>
44
72
  <% end %>
45
73
  </div>
46
74
  <div class="flex items-center gap-3 flex-shrink-0">
47
75
  <%= button_to rerun_execution_path(@execution, dry_run: true),
48
76
  method: :post,
49
- data: { turbo: false },
50
77
  class: "inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors",
51
78
  title: "Preview what would be sent without making an API call" do %>
52
79
  <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -87,21 +114,19 @@
87
114
  <%= @execution.agent_type.gsub(/Agent$/, '') %>
88
115
  </h2>
89
116
  <div class="flex flex-wrap items-center gap-2 mt-2">
90
- <%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
91
- <% if @execution.streaming? %>
92
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300">Stream</span>
93
- <% end %>
94
- <% if @execution.cache_hit %>
95
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300">Cached</span>
96
- <% end %>
97
- <% if @execution.finish_reason.present? %>
98
- <% finish_colors = {
99
- 'stop' => 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300',
100
- 'length' => 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300',
101
- 'content_filter' => 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300',
102
- 'tool_calls' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300'
103
- } %>
104
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium <%= finish_colors[@execution.finish_reason] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' %>"><%= @execution.finish_reason %></span>
117
+ <%= render "ruby_llm/agents/shared/status_badge", status: @execution.status, size: :md %>
118
+ <% secondary_badges.each do |badge| %>
119
+ <% badge_classes = case badge[:color]
120
+ when 'cyan' then 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300'
121
+ when 'purple' then 'bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300'
122
+ when 'green' then 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300'
123
+ when 'yellow' then 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300'
124
+ when 'red' then 'bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-300'
125
+ when 'blue' then 'bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300'
126
+ when 'orange' then 'bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-300'
127
+ else 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300'
128
+ end %>
129
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium <%= badge_classes %>"><%= badge[:label] %></span>
105
130
  <% end %>
106
131
  </div>
107
132
  <p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
@@ -120,7 +145,6 @@
120
145
  <div class="flex items-center gap-2">
121
146
  <%= button_to rerun_execution_path(@execution, dry_run: true),
122
147
  method: :post,
123
- data: { turbo: false },
124
148
  class: "flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-2 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors",
125
149
  title: "Preview what would be sent without making an API call" do %>
126
150
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -183,27 +207,124 @@
183
207
  </div>
184
208
  </div>
185
209
 
210
+ <!-- Workflow Summary Panel (for root workflow executions) -->
211
+ <% if @execution.respond_to?(:root_workflow?) && @execution.root_workflow? %>
212
+ <%= render "ruby_llm/agents/executions/workflow_summary", execution: @execution %>
213
+ <% end %>
214
+
215
+ <!-- Workflow Info (if applicable - for child workflow steps) -->
216
+ <% if (@execution.workflow_type.present? || @execution.workflow_step.present? || @execution.routed_to.present?) && !(@execution.respond_to?(:root_workflow?) && @execution.root_workflow?) %>
217
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 sm:p-5 mb-6">
218
+ <div class="flex items-center gap-2 mb-4">
219
+ <% case @execution.workflow_type
220
+ when "pipeline" %>
221
+ <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-sm font-medium bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300">
222
+ <span class="text-base">→</span> Pipeline Workflow
223
+ </span>
224
+ <% when "parallel" %>
225
+ <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-sm font-medium bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300">
226
+ <span class="text-base">⫴</span> Parallel Workflow
227
+ </span>
228
+ <% when "router" %>
229
+ <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-sm font-medium bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300">
230
+ <span class="text-base">⑂</span> Router Workflow
231
+ </span>
232
+ <% else %>
233
+ <% if @execution.workflow_step.present? %>
234
+ <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-sm font-medium bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300">
235
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
236
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/>
237
+ </svg>
238
+ Workflow Step
239
+ </span>
240
+ <% end %>
241
+ <% end %>
242
+ <% if @execution.workflow_id.present? %>
243
+ <span class="text-xs text-gray-400 dark:text-gray-500 font-mono" title="Workflow ID: <%= @execution.workflow_id %>">
244
+ <%= @execution.workflow_id.to_s.truncate(12) %>
245
+ </span>
246
+ <% end %>
247
+ </div>
248
+
249
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
250
+ <% if @execution.workflow_step.present? %>
251
+ <div>
252
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Step Name</p>
253
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= @execution.workflow_step %></p>
254
+ </div>
255
+ <% end %>
256
+
257
+ <% if @execution.routed_to.present? %>
258
+ <div>
259
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Routed To</p>
260
+ <p class="text-sm font-medium text-amber-600 dark:text-amber-400"><%= @execution.routed_to %></p>
261
+ </div>
262
+ <% end %>
263
+
264
+ <% if @execution.classification_result.present? %>
265
+ <%
266
+ classification = if @execution.classification_result.is_a?(String)
267
+ begin
268
+ JSON.parse(@execution.classification_result)
269
+ rescue
270
+ {}
271
+ end
272
+ else
273
+ @execution.classification_result || {}
274
+ end
275
+ %>
276
+ <% if classification["method"].present? %>
277
+ <div>
278
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Classification</p>
279
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
280
+ <%= classification["method"] == "llm" ? "LLM" : "Rule-based" %>
281
+ <% if classification["classification_time_ms"].present? %>
282
+ <span class="text-xs text-gray-400 dark:text-gray-500">(<%= classification["classification_time_ms"] %>ms)</span>
283
+ <% end %>
284
+ </p>
285
+ </div>
286
+ <% end %>
287
+ <% if classification["classifier_model"].present? %>
288
+ <div>
289
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Classifier Model</p>
290
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100 font-mono"><%= classification["classifier_model"] %></p>
291
+ </div>
292
+ <% end %>
293
+ <% end %>
294
+ </div>
295
+
296
+ <% if @execution.parent_execution_id.present? %>
297
+ <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
298
+ <span class="text-xs text-gray-500 dark:text-gray-400">Part of workflow:</span>
299
+ <%= link_to "##{@execution.parent_execution_id}",
300
+ ruby_llm_agents.execution_path(@execution.parent_execution_id),
301
+ class: "ml-2 text-blue-600 dark:text-blue-400 hover:underline font-mono text-sm" %>
302
+ </div>
303
+ <% end %>
304
+ </div>
305
+ <% end %>
306
+
186
307
  <!-- Stats Grid -->
187
308
  <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
188
- <%= render "rubyllm/agents/shared/stat_card",
309
+ <%= render "ruby_llm/agents/shared/stat_card",
189
310
  title: "Model",
190
311
  value: @execution.model_id,
191
312
  icon: "M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z",
192
313
  icon_color: "text-blue-500" %>
193
314
 
194
- <%= render "rubyllm/agents/shared/stat_card",
315
+ <%= render "ruby_llm/agents/shared/stat_card",
195
316
  title: "Duration",
196
317
  value: "#{number_to_human_short(@execution.duration_ms || 0)} ms",
197
318
  icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
198
319
  icon_color: "text-purple-500" %>
199
320
 
200
- <%= render "rubyllm/agents/shared/stat_card",
321
+ <%= render "ruby_llm/agents/shared/stat_card",
201
322
  title: "Total Tokens",
202
323
  value: number_to_human_short(@execution.total_tokens || 0),
203
324
  icon: "M7 20l4-16m2 16l4-16M6 9h14M4 15h14",
204
325
  icon_color: "text-indigo-500" %>
205
326
 
206
- <%= render "rubyllm/agents/shared/stat_card",
327
+ <%= render "ruby_llm/agents/shared/stat_card",
207
328
  title: "Total Cost",
208
329
  value: number_to_human_short(@execution.total_cost || 0, prefix: "$", precision: 2),
209
330
  icon: "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
@@ -271,7 +392,7 @@
271
392
  <div>
272
393
  <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Attempts</h3>
273
394
  <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
274
- <%= @execution.attempts_count || @execution.attempts.size %> attempt(s)
395
+ <%= @execution.respond_to?(:attempts_count) && @execution.attempts_count ? @execution.attempts_count : @execution.attempts.size %> attempt(s)
275
396
  <% if @execution.used_fallback? %>
276
397
  · <span class="text-amber-500">Used fallback model</span>
277
398
  <% end %>
@@ -280,7 +401,7 @@
280
401
  <% end %>
281
402
  </p>
282
403
  </div>
283
- <% if @execution.chosen_model_id.present? && @execution.chosen_model_id != @execution.model_id %>
404
+ <% if @execution.used_fallback? %>
284
405
  <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-300">
285
406
  Fallback: <%= @execution.chosen_model_id %>
286
407
  </span>
@@ -402,46 +523,13 @@
402
523
  </div>
403
524
  <% end %>
404
525
 
405
- <!-- Masking Toggle -->
406
- <div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-4 mb-6">
407
- <div class="flex items-center justify-between">
408
- <div class="flex items-center gap-2">
409
- <svg class="w-5 h-5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
410
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
411
- </svg>
412
- <span class="text-sm font-medium text-amber-800 dark:text-amber-200">Data Masking</span>
413
- </div>
414
- <div class="flex items-center gap-3">
415
- <span id="masking-status" class="text-xs text-amber-600 dark:text-amber-400">
416
- Sensitive data is hidden
417
- </span>
418
- <button
419
- type="button"
420
- id="toggle-masking-btn"
421
- onclick="toggleMasking()"
422
- class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-800/50"
423
- >
424
- <svg id="eye-icon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
425
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
426
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
427
- </svg>
428
- <svg id="eye-off-icon" class="w-4 h-4 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
429
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
430
- </svg>
431
- <span id="toggle-btn-text">Show Original</span>
432
- </button>
433
- </div>
434
- </div>
435
- </div>
436
-
437
526
  <!-- Parameters -->
438
527
  <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
439
528
  <div class="flex items-center justify-between mb-4">
440
529
  <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Parameters</h3>
441
530
  <button
442
531
  type="button"
443
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(redact_for_display(@execution.parameters || {}))) %>"
444
- data-copy-json-original="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.parameters || {})) %>"
532
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.parameters || {})) %>"
445
533
  class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
446
534
  >
447
535
  <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -453,8 +541,7 @@
453
541
  <span>Copy</span>
454
542
  </button>
455
543
  </div>
456
- <pre id="parameters-masked" class="maskable-content bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json_redacted(@execution.parameters || {}) %></pre>
457
- <pre id="parameters-original" class="maskable-content hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.parameters || {}) %></pre>
544
+ <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.parameters || {}) %></pre>
458
545
  </div>
459
546
 
460
547
  <!-- Response -->
@@ -464,8 +551,7 @@
464
551
  <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Response</h3>
465
552
  <button
466
553
  type="button"
467
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(redact_for_display(@execution.response))) %>"
468
- data-copy-json-original="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.response)) %>"
554
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.response)) %>"
469
555
  class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
470
556
  >
471
557
  <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -477,42 +563,52 @@
477
563
  <span>Copy</span>
478
564
  </button>
479
565
  </div>
480
- <pre id="response-masked" class="maskable-content bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono"><%= highlight_json_redacted(@execution.response) %></pre>
481
- <pre id="response-original" class="maskable-content hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono"><%= highlight_json(@execution.response) %></pre>
566
+ <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono"><%= highlight_json(@execution.response) %></pre>
482
567
  </div>
483
568
  <% end %>
484
569
 
485
570
  <!-- Tool Calls -->
486
- <% if @execution.tool_calls.present? && @execution.tool_calls.any? %>
487
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
488
- <div class="flex items-center justify-between mb-4">
571
+ <% tool_calls = @execution.tool_calls || [] %>
572
+ <% tool_call_count = tool_calls.size %>
573
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6" x-data="{ expanded: <%= tool_call_count <= 3 && tool_call_count > 0 %> }">
574
+ <div class="flex items-center justify-between mb-4">
575
+ <div class="flex items-center gap-2">
576
+ <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
577
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
578
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
579
+ </svg>
580
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Tool Calls</h3>
581
+ <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300">
582
+ <%= tool_call_count %>
583
+ </span>
584
+ </div>
585
+ <% if tool_call_count > 0 %>
489
586
  <div class="flex items-center gap-2">
490
- <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
491
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"/>
492
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
493
- </svg>
494
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Tool Calls</h3>
495
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300">
496
- <%= @execution.tool_calls.size %>
497
- </span>
587
+ <button
588
+ type="button"
589
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(tool_calls)) %>"
590
+ class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
591
+ >
592
+ <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
593
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
594
+ </svg>
595
+ <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
596
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
597
+ </svg>
598
+ <span>Copy</span>
599
+ </button>
600
+ <% if tool_call_count > 3 %>
601
+ <button type="button" @click="expanded = !expanded" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
602
+ <span x-text="expanded ? 'Collapse' : 'Expand'">Expand</span>
603
+ </button>
604
+ <% end %>
498
605
  </div>
499
- <button
500
- type="button"
501
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.tool_calls)) %>"
502
- class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
503
- >
504
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
505
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
506
- </svg>
507
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
508
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
509
- </svg>
510
- <span>Copy</span>
511
- </button>
512
- </div>
606
+ <% end %>
607
+ </div>
513
608
 
514
- <div class="space-y-4">
515
- <% @execution.tool_calls.each_with_index do |tool_call, index| %>
609
+ <% if tool_call_count > 0 %>
610
+ <div class="space-y-4" x-show="expanded" <%= tool_call_count > 3 ? 'x-cloak' : '' %>>
611
+ <% tool_calls.each_with_index do |tool_call, index| %>
516
612
  <%
517
613
  # Handle both symbol and string keys
518
614
  tool_id = tool_call['id'] || tool_call[:id]
@@ -549,31 +645,41 @@
549
645
  </div>
550
646
  <% end %>
551
647
  </div>
552
- </div>
553
- <% end %>
648
+ <% else %>
649
+ <p class="text-sm text-gray-400 dark:text-gray-500 italic">No tool calls were made during this execution.</p>
650
+ <% end %>
651
+ </div>
554
652
 
555
653
  <!-- Metadata -->
556
654
  <% if @execution.metadata.present? && @execution.metadata.any? %>
557
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
558
- <div class="flex items-center justify-between mb-4">
559
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Metadata</h3>
560
- <button
561
- type="button"
562
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(redact_for_display(@execution.metadata))) %>"
563
- data-copy-json-original="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
564
- class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
565
- >
566
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
567
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
568
- </svg>
569
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
570
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
571
- </svg>
572
- <span>Copy</span>
573
- </button>
655
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6" x-data="{ expanded: false }">
656
+ <div class="flex items-center justify-between">
657
+ <div class="flex items-center gap-2">
658
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Metadata</h3>
659
+ <span class="text-xs text-gray-400 dark:text-gray-500">(<%= @execution.metadata.keys.count %> keys)</span>
660
+ </div>
661
+ <div class="flex items-center gap-2">
662
+ <button
663
+ type="button"
664
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
665
+ class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
666
+ >
667
+ <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
668
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
669
+ </svg>
670
+ <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
671
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
672
+ </svg>
673
+ <span>Copy</span>
674
+ </button>
675
+ <button type="button" @click="expanded = !expanded" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
676
+ <span x-text="expanded ? 'Collapse' : 'Expand'">Expand</span>
677
+ </button>
678
+ </div>
679
+ </div>
680
+ <div x-show="expanded" x-cloak class="mt-4">
681
+ <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.metadata) %></pre>
574
682
  </div>
575
- <pre id="metadata-masked" class="maskable-content bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json_redacted(@execution.metadata) %></pre>
576
- <pre id="metadata-original" class="maskable-content hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.metadata) %></pre>
577
683
  </div>
578
684
  <% end %>
579
685
 
@@ -618,6 +724,7 @@
618
724
  <span id="system-prompt-toggle">Expand</span>
619
725
  </button>
620
726
  </div>
727
+ <p id="system-prompt-preview" class="text-sm text-gray-600 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded-lg p-3 truncate"><%= @execution.system_prompt.truncate(150) %></p>
621
728
  <pre id="system-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.system_prompt %></pre>
622
729
  </div>
623
730
  <% end %>
@@ -631,6 +738,7 @@
631
738
  <span id="user-prompt-toggle">Expand</span>
632
739
  </button>
633
740
  </div>
741
+ <p id="user-prompt-preview" class="text-sm text-gray-600 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded-lg p-3 truncate"><%= @execution.user_prompt.truncate(150) %></p>
634
742
  <pre id="user-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.user_prompt %></pre>
635
743
  </div>
636
744
  <% end %>
@@ -867,66 +975,18 @@
867
975
  // Prompt toggle function
868
976
  function togglePrompt(type) {
869
977
  const content = document.getElementById(type + '-prompt-content');
978
+ const preview = document.getElementById(type + '-prompt-preview');
870
979
  const toggle = document.getElementById(type + '-prompt-toggle');
871
- content.classList.toggle('hidden');
872
- toggle.textContent = content.classList.contains('hidden') ? 'Expand' : 'Collapse';
873
- }
874
-
875
- // Masking state (persisted in localStorage)
876
- let isMasked = localStorage.getItem('ruby_llm_agents_masking') !== 'false';
877
-
878
- function toggleMasking() {
879
- isMasked = !isMasked;
880
- localStorage.setItem('ruby_llm_agents_masking', isMasked);
881
- updateMaskingUI();
882
- }
883
-
884
- function updateMaskingUI() {
885
- const status = document.getElementById('masking-status');
886
- const btnText = document.getElementById('toggle-btn-text');
887
- const eyeIcon = document.getElementById('eye-icon');
888
- const eyeOffIcon = document.getElementById('eye-off-icon');
889
-
890
- // Update all maskable content sections
891
- document.querySelectorAll('[id$="-masked"]').forEach(function(el) {
892
- el.classList.toggle('hidden', !isMasked);
893
- });
894
- document.querySelectorAll('[id$="-original"]').forEach(function(el) {
895
- el.classList.toggle('hidden', isMasked);
896
- });
897
-
898
- // Update status and button
899
- if (isMasked) {
900
- status.textContent = 'Sensitive data is hidden';
901
- btnText.textContent = 'Show Original';
902
- eyeIcon.classList.remove('hidden');
903
- eyeOffIcon.classList.add('hidden');
904
- } else {
905
- status.textContent = 'Showing original data';
906
- btnText.textContent = 'Hide Sensitive';
907
- eyeIcon.classList.add('hidden');
908
- eyeOffIcon.classList.remove('hidden');
909
- }
910
-
911
- // Update copy buttons to use appropriate data
912
- document.querySelectorAll('.copy-json-btn').forEach(function(button) {
913
- const maskedData = button.getAttribute('data-copy-json');
914
- const originalData = button.getAttribute('data-copy-json-original');
915
- if (originalData) {
916
- button.setAttribute('data-active-json', isMasked ? maskedData : originalData);
917
- }
918
- });
980
+ const isHidden = content.classList.contains('hidden');
981
+ content.classList.toggle('hidden', !isHidden);
982
+ if (preview) preview.classList.toggle('hidden', isHidden);
983
+ toggle.textContent = isHidden ? 'Collapse' : 'Expand';
919
984
  }
920
985
 
921
986
  document.addEventListener('DOMContentLoaded', function() {
922
- // Initialize masking UI on page load
923
- updateMaskingUI();
924
-
925
987
  document.querySelectorAll('.copy-json-btn').forEach(function(button) {
926
988
  button.addEventListener('click', function() {
927
- // Use active JSON (considers masking state) or fall back to default
928
- const activeData = this.getAttribute('data-active-json') || this.getAttribute('data-copy-json');
929
- const jsonText = atob(activeData);
989
+ const jsonText = atob(this.getAttribute('data-copy-json'));
930
990
  const span = this.querySelector('span');
931
991
  const copyIcon = this.querySelector('.copy-icon');
932
992
  const checkIcon = this.querySelector('.check-icon');
@@ -972,8 +1032,8 @@
972
1032
  }
973
1033
  });
974
1034
 
975
- // Diagnostics panel toggle
976
- let diagnosticsExpanded = localStorage.getItem('ruby_llm_agents_diagnostics_expanded') === 'true';
1035
+ // Diagnostics panel toggle (default to expanded)
1036
+ let diagnosticsExpanded = localStorage.getItem('ruby_llm_agents_diagnostics_expanded') !== 'false';
977
1037
 
978
1038
  function toggleDiagnostics() {
979
1039
  diagnosticsExpanded = !diagnosticsExpanded;
@@ -128,7 +128,7 @@
128
128
  <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Parent Controller</p>
129
129
  <p class="text-xs text-gray-500 dark:text-gray-400">Dashboard inherits from this</p>
130
130
  </div>
131
- <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-sm text-xs"><%= @config.dashboard_parent_controller %></code>
131
+ <code class="bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-200 px-2 py-1 rounded text-xs"><%= @config.dashboard_parent_controller %></code>
132
132
  </div>
133
133
  <div class="flex justify-between items-center">
134
134
  <div>