ruby_llm-agents 1.3.4 → 2.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 (191) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -336
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +52 -12
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +54 -23
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +97 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  99. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  100. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  101. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
  102. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  103. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  104. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  105. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  106. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  107. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  108. data/lib/ruby_llm/agents/results/base.rb +1 -49
  109. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  110. data/lib/ruby_llm/agents.rb +1 -9
  111. data/lib/tasks/ruby_llm_agents.rake +34 -0
  112. metadata +14 -83
  113. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  114. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  115. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  116. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  117. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  118. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  119. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  120. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  121. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  122. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  123. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  125. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  126. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  127. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  128. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  129. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  130. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  131. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  132. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  133. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  134. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  135. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  136. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  137. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  138. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  139. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  140. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  141. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  142. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  143. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  144. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  145. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  146. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  147. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  148. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  149. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  150. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  151. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  152. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  153. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  154. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  155. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  156. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  157. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  158. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  159. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  160. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  161. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  162. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  163. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  164. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  165. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  166. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  167. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  168. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  169. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  170. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  171. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  172. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  173. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  174. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  175. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  176. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  177. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  178. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  180. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  181. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  182. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  183. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  185. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  186. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  187. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  188. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  189. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  190. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  191. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,672 +1,291 @@
1
1
  <div id="execution-detail" data-execution-id="<%= @execution.id %>" data-status="<%= @execution.status %>">
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
- ] %>
7
2
 
8
- <!-- Header -->
3
+ <!-- Back link -->
4
+ <nav class="font-mono text-xs mb-6">
5
+ <%= link_to "← executions", ruby_llm_agents.executions_path, class: "text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300" %>
6
+ </nav>
7
+
8
+ <!-- ── header ──────────────────────── -->
9
9
  <%
10
- # Collect secondary badges
11
10
  secondary_badges = []
12
- secondary_badges << { label: "Stream", color: "cyan" } if @execution.streaming?
13
- secondary_badges << { label: "Cached", color: "purple" } if @execution.cache_hit
11
+ secondary_badges << { label: "stream", css: "badge-cyan" } if @execution.streaming?
12
+ secondary_badges << { label: "cached", css: "badge-purple" } if @execution.cache_hit
14
13
  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'
14
+ finish_css = case @execution.finish_reason
15
+ when 'stop' then 'badge-success'
16
+ when 'length' then 'badge-orange'
17
+ when 'content_filter' then 'badge-error'
18
+ when 'tool_calls' then 'badge-cyan'
19
+ else 'badge-timeout'
21
20
  end
22
- secondary_badges << { label: @execution.finish_reason, color: finish_color }
21
+ secondary_badges << { label: @execution.finish_reason, css: finish_css }
23
22
  end
24
- secondary_badges << { label: "Rate Limited", color: "orange" } if @execution.respond_to?(:rate_limited?) && @execution.rate_limited?
23
+ secondary_badges << { label: "rate limited", css: "badge-orange" } if @execution.respond_to?(:rate_limited?) && @execution.rate_limited?
25
24
  %>
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">
27
- <!-- Desktop: Row 1 - Agent name + badges + buttons + date -->
28
- <div class="hidden sm:flex sm:items-center sm:justify-between gap-4">
29
- <div class="flex items-center gap-3 min-w-0">
30
- <h2 class="text-lg font-bold text-gray-900 dark:text-gray-100 truncate">
31
- <%= @execution.agent_type.gsub(/Agent$/, '') %>
32
- </h2>
33
- <%= render "ruby_llm/agents/shared/doc_link" %>
34
- <%= render "ruby_llm/agents/shared/status_badge", status: @execution.status, size: :md %>
35
- <% if secondary_badges.any? %>
36
- <div class="relative" x-data="{ showDetails: false }">
37
- <button
38
- type="button"
39
- @mouseenter="showDetails = true"
40
- @mouseleave="showDetails = false"
41
- 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"
42
- >
43
- +<%= secondary_badges.size %>
44
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
45
- <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"/>
46
- </svg>
47
- </button>
48
- <div
49
- x-show="showDetails"
50
- x-cloak
51
- x-transition:enter="transition ease-out duration-100"
52
- x-transition:enter-start="opacity-0 scale-95"
53
- x-transition:enter-end="opacity-100 scale-100"
54
- 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"
55
- >
56
- <div class="flex flex-wrap gap-1.5">
57
- <% secondary_badges.each do |badge| %>
58
- <% badge_classes = case badge[:color]
59
- when 'cyan' then 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300'
60
- when 'purple' then 'bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300'
61
- when 'green' then 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300'
62
- when 'yellow' then 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300'
63
- when 'red' then 'bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-300'
64
- when 'blue' then 'bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300'
65
- when 'orange' then 'bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-300'
66
- else 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300'
67
- end %>
68
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium <%= badge_classes %>"><%= badge[:label] %></span>
69
- <% end %>
70
- </div>
71
- </div>
72
- </div>
73
- <% end %>
74
- </div>
75
- <div class="flex items-center gap-3 flex-shrink-0">
76
- <%= button_to rerun_execution_path(@execution, dry_run: true),
77
- method: :post,
78
- 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",
79
- title: "Preview what would be sent without making an API call" do %>
80
- <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
81
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
82
- <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"/>
83
- </svg>
84
- Dry Run
85
- <% end %>
86
- <button
87
- type="button"
88
- onclick="confirmRerun()"
89
- class="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
90
- title="Re-execute this agent with the same parameters"
91
- >
92
- <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
93
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
94
- </svg>
95
- Rerun
96
- </button>
97
- <span class="text-xs text-gray-500 dark:text-gray-400 whitespace-nowrap"><%= @execution.created_at.strftime("%b %d, %H:%M") %></span>
98
- </div>
99
- </div>
100
-
101
- <!-- Desktop: Row 2 - Info line + relative time -->
102
- <div class="hidden sm:flex sm:items-center sm:justify-between mt-1.5">
103
- <p class="text-xs text-gray-500 dark:text-gray-400">
104
- #<%= @execution.id %> · v<%= @execution.agent_version %>
105
- <% if @execution.model_provider.present? %>
106
- · <%= @execution.model_provider %>
107
- <% end %>
108
- </p>
109
- <span class="text-xs text-gray-400 dark:text-gray-500"><%= time_ago_in_words(@execution.created_at) %> ago</span>
110
- </div>
111
-
112
- <!-- Mobile: Stacked layout -->
113
- <div class="sm:hidden">
114
- <h2 class="text-lg font-bold text-gray-900 dark:text-gray-100 truncate">
115
- <%= @execution.agent_type.gsub(/Agent$/, '') %>
116
- </h2>
117
- <div class="flex flex-wrap items-center gap-2 mt-2">
118
- <%= render "ruby_llm/agents/shared/status_badge", status: @execution.status, size: :md %>
119
- <% secondary_badges.each do |badge| %>
120
- <% badge_classes = case badge[:color]
121
- when 'cyan' then 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300'
122
- when 'purple' then 'bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300'
123
- when 'green' then 'bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300'
124
- when 'yellow' then 'bg-yellow-100 dark:bg-yellow-900/50 text-yellow-800 dark:text-yellow-300'
125
- when 'red' then 'bg-red-100 dark:bg-red-900/50 text-red-800 dark:text-red-300'
126
- when 'blue' then 'bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300'
127
- when 'orange' then 'bg-orange-100 dark:bg-orange-900/50 text-orange-800 dark:text-orange-300'
128
- else 'bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300'
129
- end %>
130
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-[10px] font-medium <%= badge_classes %>"><%= badge[:label] %></span>
131
- <% end %>
132
- </div>
133
- <p class="text-xs text-gray-500 dark:text-gray-400 mt-2">
134
- #<%= @execution.id %> · v<%= @execution.agent_version %>
135
- <% if @execution.model_provider.present? %>
136
- · <%= @execution.model_provider %>
137
- <% end %>
138
- </p>
139
-
140
- <!-- Mobile: Date + Buttons -->
141
- <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
142
- <p class="text-xs text-gray-500 dark:text-gray-400 mb-3">
143
- <%= @execution.created_at.strftime("%b %d, %Y at %H:%M") %>
144
- <span class="text-gray-400 dark:text-gray-500">· <%= time_ago_in_words(@execution.created_at) %> ago</span>
145
- </p>
146
- <div class="flex items-center gap-2">
147
- <%= button_to rerun_execution_path(@execution, dry_run: true),
148
- method: :post,
149
- 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",
150
- title: "Preview what would be sent without making an API call" do %>
151
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
152
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
153
- <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"/>
154
- </svg>
155
- Dry Run
156
- <% end %>
157
- <button
158
- type="button"
159
- onclick="confirmRerun()"
160
- class="flex-1 inline-flex items-center justify-center gap-1.5 px-3 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
161
- title="Re-execute this agent with the same parameters"
162
- >
163
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
164
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
165
- </svg>
166
- Rerun
167
- </button>
168
- </div>
169
- </div>
25
+ <div class="flex flex-wrap items-center gap-3 mb-1.5">
26
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono"><%= @execution.agent_type.gsub(/Agent$/, '') %></span>
27
+ <div class="font-mono text-xs text-gray-400 dark:text-gray-500 flex flex-wrap items-center gap-1.5">
28
+ <%= render "ruby_llm/agents/shared/status_badge", status: @execution.status %>
29
+ <% secondary_badges.each do |badge| %>
30
+ <span class="badge badge-sm <%= badge[:css] %>"><%= badge[:label] %></span>
31
+ <% end %>
32
+ <span class="text-gray-800 dark:text-gray-200"><%= @execution.model_id %></span>
33
+ <% if @execution.model_provider.present? %>
34
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
35
+ <span><%= @execution.model_provider %></span>
36
+ <% end %>
170
37
  </div>
38
+ <%= render "ruby_llm/agents/shared/doc_link" %>
39
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
171
40
  </div>
172
41
 
173
- <!-- Rerun Confirmation Modal -->
174
- <div id="rerun-modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
175
- <div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
176
- <div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75 dark:bg-gray-900 dark:bg-opacity-75" onclick="closeRerunModal()"></div>
177
-
178
- <div class="relative inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-gray-800 rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
179
- <div class="sm:flex sm:items-start">
180
- <div class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-blue-100 dark:bg-blue-900/30 rounded-full sm:mx-0 sm:h-10 sm:w-10">
181
- <svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
182
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
183
- </svg>
184
- </div>
185
- <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
186
- <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">Confirm Rerun</h3>
187
- <div class="mt-2">
188
- <p class="text-sm text-gray-500 dark:text-gray-400">
189
- This will re-execute the agent with the original parameters. A new execution record will be created and the agent will make a real API call.
190
- </p>
191
- <p class="mt-2 text-sm text-amber-600 dark:text-amber-400">
192
- This action may incur API costs.
193
- </p>
194
- </div>
195
- </div>
196
- </div>
197
- <div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-3">
198
- <%= button_to rerun_execution_path(@execution),
199
- method: :post,
200
- class: "inline-flex justify-center w-full px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto" do %>
201
- Confirm Rerun
202
- <% end %>
203
- <button type="button" onclick="closeRerunModal()" class="inline-flex justify-center w-full px-4 py-2 mt-3 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-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none sm:mt-0 sm:w-auto">
204
- Cancel
205
- </button>
206
- </div>
207
- </div>
208
- </div>
42
+ <div class="font-mono text-xs text-gray-400 dark:text-gray-500 mb-1">
43
+ #<%= @execution.id %>
44
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
45
+ <%= @execution.created_at.strftime("%b %d, %H:%M") %>
46
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
47
+ <%= time_ago_in_words(@execution.created_at) %> ago
209
48
  </div>
210
49
 
211
- <!-- Workflow Summary Panel (for root workflow executions) -->
212
- <% if @execution.respond_to?(:root_workflow?) && @execution.root_workflow? %>
213
- <%= render "ruby_llm/agents/executions/workflow_summary", execution: @execution %>
214
- <% end %>
215
-
216
- <!-- Workflow Info (if applicable - for child workflow steps) -->
217
- <% if (@execution.workflow_type.present? || @execution.workflow_step.present? || @execution.routed_to.present?) && !(@execution.respond_to?(:root_workflow?) && @execution.root_workflow?) %>
218
- <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">
219
- <div class="flex items-center gap-2 mb-4">
220
- <% if @execution.workflow_type.present? %>
221
- <span class="inline-flex items-center gap-1.5 px-2.5 py-1 rounded-md text-sm font-medium bg-emerald-100 dark:bg-emerald-900/50 text-emerald-700 dark:text-emerald-300">
222
- <span class="text-base">◈</span> Workflow
223
- </span>
224
- <% elsif @execution.workflow_step.present? %>
225
- <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">
226
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
227
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/>
228
- </svg>
229
- Workflow Step
230
- </span>
231
- <% end %>
232
- <% if @execution.workflow_id.present? %>
233
- <span class="text-xs text-gray-400 dark:text-gray-500 font-mono" title="Workflow ID: <%= @execution.workflow_id %>">
234
- <%= @execution.workflow_id.to_s.truncate(12) %>
235
- </span>
236
- <% end %>
237
- </div>
238
-
239
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4">
240
- <% if @execution.workflow_step.present? %>
241
- <div>
242
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Step Name</p>
243
- <p class="text-sm font-medium text-gray-900 dark:text-gray-100"><%= @execution.workflow_step %></p>
244
- </div>
245
- <% end %>
246
-
247
- <% if @execution.routed_to.present? %>
248
- <div>
249
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Routed To</p>
250
- <p class="text-sm font-medium text-amber-600 dark:text-amber-400"><%= @execution.routed_to %></p>
251
- </div>
252
- <% end %>
253
-
254
- <% if @execution.classification_result.present? %>
255
- <%
256
- classification = if @execution.classification_result.is_a?(String)
257
- begin
258
- JSON.parse(@execution.classification_result)
259
- rescue
260
- {}
261
- end
262
- else
263
- @execution.classification_result || {}
264
- end
265
- %>
266
- <% if classification["method"].present? %>
267
- <div>
268
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Classification</p>
269
- <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
270
- <%= classification["method"] == "llm" ? "LLM" : "Rule-based" %>
271
- <% if classification["classification_time_ms"].present? %>
272
- <span class="text-xs text-gray-400 dark:text-gray-500">(<%= classification["classification_time_ms"] %>ms)</span>
273
- <% end %>
274
- </p>
275
- </div>
276
- <% end %>
277
- <% if classification["classifier_model"].present? %>
278
- <div>
279
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Classifier Model</p>
280
- <p class="text-sm font-medium text-gray-900 dark:text-gray-100 font-mono"><%= classification["classifier_model"] %></p>
281
- </div>
282
- <% end %>
283
- <% end %>
284
- </div>
285
-
286
- <% if @execution.parent_execution_id.present? %>
287
- <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
288
- <span class="text-xs text-gray-500 dark:text-gray-400">Part of workflow:</span>
289
- <%= link_to "##{@execution.parent_execution_id}",
290
- ruby_llm_agents.execution_path(@execution.parent_execution_id),
291
- class: "ml-2 text-blue-600 dark:text-blue-400 hover:underline font-mono text-sm" %>
292
- </div>
293
- <% end %>
294
- </div>
295
- <% end %>
296
-
297
- <!-- Stats Grid -->
298
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
299
- <%= render "ruby_llm/agents/shared/stat_card",
300
- title: "Model",
301
- value: @execution.model_id,
302
- 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",
303
- icon_color: "text-blue-500" %>
304
-
305
- <%= render "ruby_llm/agents/shared/stat_card",
306
- title: "Duration",
307
- value: "#{number_to_human_short(@execution.duration_ms || 0)} ms",
308
- icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
309
- icon_color: "text-purple-500" %>
310
-
311
- <%= render "ruby_llm/agents/shared/stat_card",
312
- title: "Total Tokens",
313
- value: number_to_human_short(@execution.total_tokens || 0),
314
- icon: "M7 20l4-16m2 16l4-16M6 9h14M4 15h14",
315
- icon_color: "text-indigo-500" %>
316
-
317
- <%= render "ruby_llm/agents/shared/stat_card",
318
- title: "Total Cost",
319
- value: number_to_human_short(@execution.total_cost || 0, prefix: "$", precision: 2),
320
- 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",
321
- icon_color: "text-amber-500" %>
50
+ <!-- Stats inline row -->
51
+ <div class="flex flex-wrap items-center gap-x-4 gap-y-1 font-mono text-xs text-gray-400 dark:text-gray-500 mb-2">
52
+ <span><span class="text-gray-800 dark:text-gray-200"><%= number_to_human_short(@execution.duration_ms || 0) %>ms</span> duration</span>
53
+ <span><span class="text-gray-800 dark:text-gray-200"><%= number_to_human_short(@execution.total_tokens || 0) %></span> tokens</span>
54
+ <span><span class="text-gray-800 dark:text-gray-200"><%= number_to_human_short(@execution.total_cost || 0, prefix: "$", precision: 2) %></span> cost</span>
55
+ <% if @execution.tokens_per_second %>
56
+ <span><span class="text-gray-800 dark:text-gray-200"><%= @execution.tokens_per_second.round(1) %></span> tok/s</span>
57
+ <% end %>
322
58
  </div>
323
59
 
324
- <!-- Token Breakdown -->
325
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
326
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-4">Token Usage</h3>
60
+ <!-- ── tokens ──────────────────────── -->
61
+ <%
62
+ input_tokens = @execution.input_tokens || 0
63
+ output_tokens = @execution.output_tokens || 0
64
+ total = input_tokens + output_tokens
65
+ input_pct = total > 0 ? (input_tokens.to_f / total * 100).round(1) : 0
66
+ output_pct = total > 0 ? (output_tokens.to_f / total * 100).round(1) : 0
67
+ %>
68
+ <div class="flex items-center gap-3 mt-6 mb-3">
69
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">tokens</span>
70
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
71
+ </div>
327
72
 
328
- <!-- Token Distribution Bar -->
329
- <%
330
- input_tokens = @execution.input_tokens || 0
331
- output_tokens = @execution.output_tokens || 0
332
- total = input_tokens + output_tokens
333
- input_pct = total > 0 ? (input_tokens.to_f / total * 100).round(1) : 0
334
- output_pct = total > 0 ? (output_tokens.to_f / total * 100).round(1) : 0
335
- %>
336
- <div class="mb-6">
337
- <div class="flex justify-between text-xs mb-1.5">
338
- <span class="text-blue-600 dark:text-blue-400 font-medium">Input: <%= number_to_human_short(input_tokens) %> (<%= input_pct %>%)</span>
339
- <span class="text-green-600 dark:text-green-400 font-medium">Output: <%= number_to_human_short(output_tokens) %> (<%= output_pct %>%)</span>
340
- </div>
341
- <div class="h-2.5 bg-gray-100 dark:bg-gray-700 rounded-full overflow-hidden flex">
342
- <div class="bg-blue-500 transition-all" style="width: <%= input_pct %>%"></div>
343
- <div class="bg-green-500 transition-all" style="width: <%= output_pct %>%"></div>
344
- </div>
73
+ <div class="mb-2">
74
+ <div class="flex justify-between text-xs font-mono mb-1">
75
+ <span class="text-blue-600 dark:text-blue-400">input: <%= number_to_human_short(input_tokens) %> (<%= input_pct %>%)</span>
76
+ <span class="text-green-600 dark:text-green-400">output: <%= number_to_human_short(output_tokens) %> (<%= output_pct %>%)</span>
345
77
  </div>
346
-
347
- <!-- Detailed Metrics -->
348
- <div class="grid grid-cols-2 md:grid-cols-5 gap-6 pt-4 border-t border-gray-100 dark:border-gray-700">
349
- <div>
350
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Input</p>
351
- <p class="text-lg font-semibold text-gray-900 dark:text-gray-100"><%= number_to_human_short(@execution.input_tokens || 0) %></p>
352
- <p class="text-xs text-gray-400 dark:text-gray-500"><%= number_to_human_short(@execution.input_cost || 0, prefix: "$", precision: 4) %></p>
353
- </div>
354
-
355
- <div>
356
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Output</p>
357
- <p class="text-lg font-semibold text-gray-900 dark:text-gray-100"><%= number_to_human_short(@execution.output_tokens || 0) %></p>
358
- <p class="text-xs text-gray-400 dark:text-gray-500"><%= number_to_human_short(@execution.output_cost || 0, prefix: "$", precision: 4) %></p>
359
- </div>
360
-
361
- <div>
362
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Cached</p>
363
- <p class="text-lg font-semibold text-gray-900 dark:text-gray-100"><%= number_to_human_short(@execution.cached_tokens || 0) %></p>
364
- </div>
365
-
366
- <div>
367
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Cache Creation</p>
368
- <p class="text-lg font-semibold text-gray-900 dark:text-gray-100"><%= number_to_human_short(@execution.cache_creation_tokens || 0) %></p>
369
- </div>
370
-
371
- <div>
372
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide">Tokens/Sec</p>
373
- <p class="text-lg font-semibold text-gray-900 dark:text-gray-100"><%= @execution.tokens_per_second || 'N/A' %></p>
374
- </div>
78
+ <div class="h-1.5 bg-gray-100 dark:bg-gray-800 rounded-full overflow-hidden flex">
79
+ <div class="bg-blue-500 transition-all" style="width: <%= input_pct %>%"></div>
80
+ <div class="bg-green-500 transition-all" style="width: <%= output_pct %>%"></div>
375
81
  </div>
376
82
  </div>
377
83
 
378
- <!-- Attempts Table (for reliability-enabled executions) -->
84
+ <div class="flex flex-wrap items-center gap-x-4 gap-y-1 font-mono text-xs text-gray-400 dark:text-gray-500">
85
+ <span><span class="text-gray-800 dark:text-gray-200"><%= number_to_human_short(input_tokens) %></span> input <span class="text-gray-300 dark:text-gray-700">(<%= number_to_human_short(@execution.input_cost || 0, prefix: "$", precision: 4) %>)</span></span>
86
+ <span><span class="text-gray-800 dark:text-gray-200"><%= number_to_human_short(output_tokens) %></span> output <span class="text-gray-300 dark:text-gray-700">(<%= number_to_human_short(@execution.output_cost || 0, prefix: "$", precision: 4) %>)</span></span>
87
+ <span><span class="text-gray-800 dark:text-gray-200"><%= number_to_human_short(@execution.cached_tokens || 0) %></span> cached</span>
88
+ <span><span class="text-gray-800 dark:text-gray-200"><%= number_to_human_short(@execution.cache_creation_tokens || 0) %></span> cache creation</span>
89
+ </div>
90
+
91
+ <!-- ── attempts ──────────────────────── -->
379
92
  <% if @execution.respond_to?(:attempts) && @execution.attempts.present? %>
380
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
381
- <div class="flex items-center justify-between mb-4">
382
- <div>
383
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Attempts</h3>
384
- <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
385
- <%= @execution.respond_to?(:attempts_count) && @execution.attempts_count ? @execution.attempts_count : @execution.attempts.size %> attempt(s)
386
- <% if @execution.used_fallback? %>
387
- · <span class="text-amber-500">Used fallback model</span>
388
- <% end %>
389
- <% if @execution.has_retries? %>
390
- · <span class="text-blue-500">Retried</span>
391
- <% end %>
392
- </p>
393
- </div>
93
+ <div class="flex items-center gap-3 mt-6 mb-3">
94
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">attempts (<%= @execution.respond_to?(:attempts_count) && @execution.attempts_count ? @execution.attempts_count : @execution.attempts.size %>)</span>
95
+ <div class="font-mono text-xs text-gray-400 dark:text-gray-500 flex items-center gap-1.5">
394
96
  <% if @execution.used_fallback? %>
395
- <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">
396
- Fallback: <%= @execution.chosen_model_id %>
397
- </span>
97
+ <span class="badge badge-sm badge-orange">fallback: <%= @execution.chosen_model_id %></span>
98
+ <% end %>
99
+ <% if @execution.has_retries? %>
100
+ <span class="badge badge-sm badge-cyan">retried</span>
398
101
  <% end %>
399
102
  </div>
103
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
104
+ </div>
400
105
 
401
- <% error_attempts = @execution.attempts.select { |a| a['error_class'].present? } %>
402
- <% if error_attempts.any? %>
403
- <script type="application/json" id="all-errors-data">
404
- <%= raw error_attempts.map { |a|
405
- lines = ["#{a['error_class']}: #{a['error_message']}"]
406
- lines += (a['error_backtrace'] || [])
407
- "Model: #{a['model_id']}\n#{lines.join("\n")}"
408
- }.join("\n\n---\n\n").to_json %>
409
- </script>
410
- <% end %>
106
+ <% error_attempts = @execution.attempts.select { |a| a['error_class'].present? } %>
107
+ <% if error_attempts.any? %>
108
+ <script type="application/json" id="all-errors-data">
109
+ <%= raw error_attempts.map { |a|
110
+ lines = ["#{a['error_class']}: #{a['error_message']}"]
111
+ lines += (a['error_backtrace'] || [])
112
+ "Model: #{a['model_id']}\n#{lines.join("\n")}"
113
+ }.join("\n\n---\n\n").to_json %>
114
+ </script>
115
+ <div class="flex justify-end mb-2">
116
+ <button onclick="var text = JSON.parse(document.getElementById('all-errors-data').textContent); navigator.clipboard.writeText(text).then(function() { var btn = event.currentTarget; btn.textContent = 'copied!'; setTimeout(function() { btn.textContent = 'copy all errors'; }, 2000); });"
117
+ class="font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
118
+ copy all errors
119
+ </button>
120
+ </div>
121
+ <% end %>
411
122
 
412
- <div class="overflow-x-auto">
413
- <% if error_attempts.any? %>
414
- <div class="flex justify-end mb-2">
415
- <button onclick="var text = JSON.parse(document.getElementById('all-errors-data').textContent); navigator.clipboard.writeText(text).then(function() { var btn = event.currentTarget; btn.textContent = 'Copied!'; setTimeout(function() { btn.textContent = 'Copy all errors'; }, 2000); });"
416
- class="inline-flex items-center gap-1.5 px-2.5 py-1 text-xs font-medium text-gray-600 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors">
417
- <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
418
- Copy all errors
419
- </button>
420
- </div>
421
- <% end %>
422
- <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
423
- <thead>
424
- <tr>
425
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">#</th>
426
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Model</th>
427
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
428
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Duration</th>
429
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tokens</th>
430
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Error</th>
431
- </tr>
432
- </thead>
433
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
434
- <% @execution.attempts.each_with_index do |attempt, index| %>
435
- <tr class="<%= attempt['short_circuited'] ? 'bg-gray-50 dark:bg-gray-900/50' : '' %>">
436
- <td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
437
- <%= index + 1 %>
438
- </td>
439
- <td class="px-3 py-2 text-sm font-mono text-gray-900 dark:text-gray-100">
440
- <%= attempt['model_id'] %>
441
- </td>
442
- <td class="px-3 py-2">
443
- <% if attempt['short_circuited'] %>
444
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
445
- <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
446
- <path fill-rule="evenodd" d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z" clip-rule="evenodd"/>
447
- </svg>
448
- Blocked
449
- </span>
450
- <% elsif attempt['error_class'].present? %>
451
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300">
452
- <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
453
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
454
- </svg>
455
- Failed
456
- </span>
457
- <% else %>
458
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300">
459
- <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
460
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
461
- </svg>
462
- Success
463
- </span>
464
- <% end %>
465
- </td>
466
- <td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
467
- <%= attempt['duration_ms'] ? "#{attempt['duration_ms']}ms" : '-' %>
468
- </td>
469
- <td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
470
- <% if attempt['input_tokens'] || attempt['output_tokens'] %>
471
- <span class="text-blue-600 dark:text-blue-400"><%= attempt['input_tokens'] || 0 %></span>
472
- /
473
- <span class="text-green-600 dark:text-green-400"><%= attempt['output_tokens'] || 0 %></span>
474
- <% else %>
475
- -
123
+ <div class="overflow-x-auto">
124
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-800">
125
+ <thead>
126
+ <tr>
127
+ <th class="px-3 py-2 text-left text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono">#</th>
128
+ <th class="px-3 py-2 text-left text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono">model</th>
129
+ <th class="px-3 py-2 text-left text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono">status</th>
130
+ <th class="px-3 py-2 text-left text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono">duration</th>
131
+ <th class="px-3 py-2 text-left text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono">tokens</th>
132
+ <th class="px-3 py-2 text-left text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono">error</th>
133
+ </tr>
134
+ </thead>
135
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-800">
136
+ <% @execution.attempts.each_with_index do |attempt, index| %>
137
+ <tr class="<%= attempt['short_circuited'] ? 'bg-gray-50 dark:bg-gray-900/50' : '' %>">
138
+ <td class="px-3 py-2 text-xs font-mono text-gray-500 dark:text-gray-400"><%= index + 1 %></td>
139
+ <td class="px-3 py-2 text-xs font-mono text-gray-900 dark:text-gray-100"><%= attempt['model_id'] %></td>
140
+ <td class="px-3 py-2">
141
+ <% if attempt['short_circuited'] %>
142
+ <span class="badge badge-sm badge-timeout">blocked</span>
143
+ <% elsif attempt['error_class'].present? %>
144
+ <span class="badge badge-sm badge-error">failed</span>
145
+ <% else %>
146
+ <span class="badge badge-sm badge-success">success</span>
147
+ <% end %>
148
+ </td>
149
+ <td class="px-3 py-2 text-xs font-mono text-gray-500 dark:text-gray-400">
150
+ <%= attempt['duration_ms'] ? "#{attempt['duration_ms']}ms" : '-' %>
151
+ </td>
152
+ <td class="px-3 py-2 text-xs font-mono text-gray-500 dark:text-gray-400">
153
+ <% if attempt['input_tokens'] || attempt['output_tokens'] %>
154
+ <span class="text-blue-600 dark:text-blue-400"><%= attempt['input_tokens'] || 0 %></span>
155
+ /
156
+ <span class="text-green-600 dark:text-green-400"><%= attempt['output_tokens'] || 0 %></span>
157
+ <% else %>
158
+ -
159
+ <% end %>
160
+ </td>
161
+ <td class="px-3 py-2 text-xs max-w-xs">
162
+ <% if attempt['error_class'].present? %>
163
+ <span class="text-red-600 dark:text-red-400 font-mono font-medium"><%= attempt['error_class'].split('::').last %></span>
164
+ <p class="text-red-500 dark:text-red-400 text-xs mt-0.5 break-words"><%= attempt['error_message'].to_s.truncate(150) %></p>
165
+ <% if attempt['error_backtrace'].present? %>
166
+ <button onclick="var el = document.getElementById('backtrace-<%= index %>'); el.classList.toggle('hidden');"
167
+ class="text-xs font-mono text-gray-400 dark:text-gray-500 hover:text-red-500 dark:hover:text-red-400 mt-1 underline">
168
+ stack trace
169
+ </button>
476
170
  <% end %>
477
- </td>
478
- <td class="px-3 py-2 text-sm max-w-xs">
479
- <% if attempt['error_class'].present? %>
480
- <div>
481
- <span class="text-red-600 dark:text-red-400 font-mono text-xs font-medium">
482
- <%= attempt['error_class'].split('::').last %>
483
- </span>
171
+ <% else %>
172
+ <span class="text-gray-400">-</span>
173
+ <% end %>
174
+ </td>
175
+ </tr>
176
+ <% if attempt['error_backtrace'].present? %>
177
+ <tr id="backtrace-<%= index %>" class="hidden">
178
+ <td colspan="6" class="px-3 py-2">
179
+ <div class="bg-gray-900 text-gray-100 text-xs font-mono p-3 rounded max-h-64 overflow-auto whitespace-pre-wrap relative group/bt">
180
+ <button onclick="var el = document.getElementById('backtrace-text-<%= index %>'); navigator.clipboard.writeText(el.textContent.trim()).then(function() { var btn = event.currentTarget; btn.textContent = 'copied!'; setTimeout(function() { btn.textContent = 'copy'; }, 2000); });"
181
+ class="absolute top-2 right-2 text-xs font-mono text-gray-400 hover:text-white transition-colors">
182
+ copy
183
+ </button>
184
+ <div id="backtrace-text-<%= index %>">
185
+ <div class="font-semibold text-red-400 mb-1"><%= attempt['error_class'] %>: <%= attempt['error_message'].to_s.truncate(200) %></div>
186
+ <% attempt['error_backtrace'].each do |line| %>
187
+ <div class="text-gray-300 leading-relaxed"><%= line %></div>
188
+ <% end %>
484
189
  </div>
485
- <p class="text-red-500 dark:text-red-400 text-xs mt-0.5 break-words">
486
- <%= attempt['error_message'].to_s.truncate(150) %>
487
- </p>
488
- <% if attempt['error_backtrace'].present? %>
489
- <button onclick="var el = document.getElementById('backtrace-<%= index %>'); el.classList.toggle('hidden');"
490
- class="text-xs text-gray-500 dark:text-gray-400 hover:text-red-500 dark:hover:text-red-400 mt-1 underline">
491
- Stack trace
492
- </button>
493
- <% end %>
494
- <% else %>
495
- <span class="text-gray-400">-</span>
496
- <% end %>
190
+ </div>
497
191
  </td>
498
192
  </tr>
499
- <% if attempt['error_backtrace'].present? %>
500
- <tr id="backtrace-<%= index %>" class="hidden">
501
- <td colspan="6" class="px-3 py-2">
502
- <div class="bg-gray-900 text-gray-100 text-xs font-mono p-3 rounded-lg max-h-64 overflow-auto whitespace-pre-wrap relative group/bt">
503
- <button onclick="var el = document.getElementById('backtrace-text-<%= index %>'); navigator.clipboard.writeText(el.textContent.trim()).then(function() { var btn = event.currentTarget; btn.querySelector('span').textContent = 'Copied!'; setTimeout(function() { btn.querySelector('span').textContent = 'Copy'; }, 2000); });"
504
- class="absolute top-2 right-2 inline-flex items-center gap-1 px-2 py-0.5 text-xs text-gray-400 hover:text-white bg-gray-800 hover:bg-gray-700 rounded transition-colors">
505
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"/></svg>
506
- <span>Copy</span>
507
- </button>
508
- <div id="backtrace-text-<%= index %>">
509
- <div class="font-semibold text-red-400 mb-1"><%= attempt['error_class'] %>: <%= attempt['error_message'].to_s.truncate(200) %></div>
510
- <% attempt['error_backtrace'].each do |line| %>
511
- <div class="text-gray-300 leading-relaxed"><%= line %></div>
512
- <% end %>
513
- </div>
514
- </div>
515
- </td>
516
- </tr>
517
- <% end %>
518
193
  <% end %>
519
- </tbody>
520
- </table>
521
- </div>
522
-
523
- <% if @execution.fallback_chain.present? && @execution.fallback_chain.any? %>
524
- <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
525
- <p class="text-xs text-gray-500 dark:text-gray-400">
526
- <span class="font-medium">Fallback chain:</span>
527
- <%= @execution.fallback_chain.join(' → ') %>
528
- </p>
529
- <% if @execution.fallback_reason.present? %>
530
- <p class="text-xs text-amber-600 dark:text-amber-400 mt-1">
531
- Fallback reason: <%= @execution.fallback_reason %>
532
- </p>
533
194
  <% end %>
534
- </div>
535
- <% end %>
195
+ </tbody>
196
+ </table>
536
197
  </div>
537
- <% end %>
538
198
 
539
- <% if @execution.status_error? %>
540
- <!-- Error Details -->
541
- <div class="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg p-6 mb-6">
542
- <div class="flex items-center justify-between mb-2">
543
- <h3 class="text-lg font-semibold text-red-800 dark:text-red-300">Error Details</h3>
544
- <div class="flex items-center gap-2">
545
- <% if @execution.rate_limited? %>
546
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300">
547
- Rate Limited
548
- </span>
549
- <% end %>
550
- <% if @execution.retryable %>
551
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
552
- Retryable
553
- </span>
554
- <% end %>
555
- <button
556
- type="button"
557
- data-copy-json="<%= Base64.strict_encode64({
558
- error_class: @execution.error_class,
559
- error_message: @execution.error_message
560
- }.to_json) %>"
561
- 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"
562
- >
563
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
564
- <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"/>
565
- </svg>
566
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
567
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
568
- </svg>
569
- <span>Copy</span>
570
- </button>
571
- </div>
199
+ <% if @execution.fallback_chain.present? && @execution.fallback_chain.any? %>
200
+ <div class="mt-3 font-mono text-xs text-gray-400 dark:text-gray-500">
201
+ <span class="text-gray-800 dark:text-gray-200">fallback chain:</span>
202
+ <%= @execution.fallback_chain.join(' ') %>
203
+ <% if @execution.fallback_reason.present? %>
204
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
205
+ <span class="text-amber-600 dark:text-amber-400"><%= @execution.fallback_reason %></span>
206
+ <% end %>
572
207
  </div>
573
-
574
- <p class="font-mono text-sm text-red-700 dark:text-red-400 mb-2">
575
- <%= @execution.error_class %>
576
- </p>
577
-
578
- <pre class="bg-red-100 dark:bg-red-900/50 rounded p-4 text-sm text-red-900 dark:text-red-200 overflow-x-auto"><%= @execution.error_message %></pre>
579
- </div>
208
+ <% end %>
580
209
  <% end %>
581
210
 
582
- <!-- Parameters -->
583
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
584
- <div class="flex items-center justify-between mb-4">
585
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Parameters</h3>
211
+ <!-- ── error ──────────────────────── -->
212
+ <% if @execution.status_error? %>
213
+ <div class="flex items-center gap-3 mt-6 mb-3">
214
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">error</span>
215
+ <div class="font-mono text-xs text-gray-400 dark:text-gray-500 flex items-center gap-1.5">
216
+ <% if @execution.rate_limited? %>
217
+ <span class="badge badge-sm badge-orange">rate limited</span>
218
+ <% end %>
219
+ <% if @execution.retryable %>
220
+ <span class="badge badge-sm badge-cyan">retryable</span>
221
+ <% end %>
222
+ </div>
223
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
586
224
  <button
587
225
  type="button"
588
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.parameters || {})) %>"
589
- 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"
590
- >
591
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
592
- <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"/>
593
- </svg>
594
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
595
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
596
- </svg>
597
- <span>Copy</span>
598
- </button>
226
+ data-copy-json="<%= Base64.strict_encode64({
227
+ error_class: @execution.error_class,
228
+ error_message: @execution.error_message
229
+ }.to_json) %>"
230
+ class="copy-json-btn font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
231
+ >copy</button>
599
232
  </div>
600
- <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>
233
+
234
+ <p class="font-mono text-xs text-red-700 dark:text-red-400 mb-2"><%= @execution.error_class %></p>
235
+ <pre class="bg-red-50 dark:bg-red-500/10 text-red-900 dark:text-red-200 rounded p-4 text-xs overflow-x-auto font-mono"><%= @execution.error_message %></pre>
236
+ <% end %>
237
+
238
+ <!-- ── parameters ──────────────────── -->
239
+ <div class="flex items-center gap-3 mt-6 mb-3">
240
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">parameters</span>
241
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
242
+ <button
243
+ type="button"
244
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.parameters || {})) %>"
245
+ class="copy-json-btn font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
246
+ >copy</button>
601
247
  </div>
248
+ <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded p-4 text-xs overflow-x-auto font-mono"><%= highlight_json(@execution.parameters || {}) %></pre>
602
249
 
603
- <!-- Response -->
250
+ <!-- ── response ──────────────────────── -->
604
251
  <% if @execution.response.present? %>
605
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
606
- <div class="flex items-center justify-between mb-4">
607
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Response</h3>
608
- <button
609
- type="button"
610
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.response)) %>"
611
- 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"
612
- >
613
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
614
- <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"/>
615
- </svg>
616
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
617
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
618
- </svg>
619
- <span>Copy</span>
620
- </button>
621
- </div>
622
- <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>
252
+ <div class="flex items-center gap-3 mt-6 mb-3">
253
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">response</span>
254
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
255
+ <button
256
+ type="button"
257
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.response)) %>"
258
+ class="copy-json-btn font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
259
+ >copy</button>
623
260
  </div>
261
+ <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded p-4 text-xs overflow-x-auto max-h-96 font-mono"><%= highlight_json(@execution.response) %></pre>
624
262
  <% end %>
625
263
 
626
- <!-- Tool Calls -->
264
+ <!-- ── tool calls ──────────────────── -->
627
265
  <% tool_calls = @execution.tool_calls || [] %>
628
266
  <% tool_call_count = tool_calls.size %>
629
- <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 %> }">
630
- <div class="flex items-center justify-between mb-4">
631
- <div class="flex items-center gap-2">
632
- <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
633
- <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"/>
634
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
635
- </svg>
636
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Tool Calls</h3>
637
- <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">
638
- <%= tool_call_count %>
639
- </span>
640
- </div>
267
+ <div x-data="{ expanded: <%= tool_call_count <= 3 && tool_call_count > 0 %> }">
268
+ <div class="flex items-center gap-3 mt-6 mb-3">
269
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">tool calls (<%= tool_call_count %>)</span>
270
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
641
271
  <% if tool_call_count > 0 %>
642
- <div class="flex items-center gap-2">
643
- <button
644
- type="button"
645
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(tool_calls)) %>"
646
- 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"
647
- >
648
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
649
- <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"/>
650
- </svg>
651
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
652
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
653
- </svg>
654
- <span>Copy</span>
272
+ <button
273
+ type="button"
274
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(tool_calls)) %>"
275
+ class="copy-json-btn font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
276
+ >copy</button>
277
+ <% if tool_call_count > 3 %>
278
+ <button type="button" @click="expanded = !expanded" class="font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
279
+ <span x-text="expanded ? 'collapse' : 'expand'">expand</span>
655
280
  </button>
656
- <% if tool_call_count > 3 %>
657
- <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">
658
- <span x-text="expanded ? 'Collapse' : 'Expand'">Expand</span>
659
- </button>
660
- <% end %>
661
- </div>
281
+ <% end %>
662
282
  <% end %>
663
283
  </div>
664
284
 
665
285
  <% if tool_call_count > 0 %>
666
- <div class="space-y-4" x-show="expanded" x-cloak>
286
+ <div class="space-y-3" x-show="expanded" x-cloak>
667
287
  <% tool_calls.each_with_index do |tool_call, index| %>
668
288
  <%
669
- # Handle both symbol and string keys for backward compatibility
670
289
  tool_id = tool_call['id'] || tool_call[:id]
671
290
  tool_name = tool_call['name'] || tool_call[:name]
672
291
  tool_args = tool_call['arguments'] || tool_call[:arguments] || {}
@@ -676,480 +295,403 @@
676
295
  tool_duration = tool_call['duration_ms'] || tool_call[:duration_ms]
677
296
  tool_called_at = tool_call['called_at'] || tool_call[:called_at]
678
297
 
679
- # Status badge styling
680
- status_badge_class = case tool_status
681
- when 'success' then 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300'
682
- when 'error' then 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300'
683
- else 'bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300'
298
+ status_css = case tool_status
299
+ when 'success' then 'badge-success'
300
+ when 'error' then 'badge-error'
301
+ else 'badge-timeout'
684
302
  end
685
303
  %>
686
- <div class="border border-gray-100 dark:border-gray-700 rounded-lg overflow-hidden">
687
- <!-- Tool Call Header -->
688
- <div class="bg-gray-50 dark:bg-gray-900/50 px-4 py-3">
304
+ <div class="border border-gray-200 dark:border-gray-800 rounded overflow-hidden">
305
+ <div class="bg-gray-50 dark:bg-gray-900/50 px-3 py-2">
689
306
  <div class="flex items-center justify-between">
690
- <div class="flex items-center gap-3">
691
- <span class="inline-flex items-center justify-center w-6 h-6 rounded-full bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 text-xs font-medium">
692
- <%= index + 1 %>
693
- </span>
694
- <code class="text-sm font-semibold text-gray-900 dark:text-gray-100"><%= tool_name %></code>
695
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= status_badge_class %>">
696
- <%= tool_status %>
697
- </span>
307
+ <div class="flex items-center gap-2 font-mono text-xs">
308
+ <span class="text-gray-400 dark:text-gray-600"><%= index + 1 %></span>
309
+ <code class="font-semibold text-gray-900 dark:text-gray-100"><%= tool_name %></code>
310
+ <span class="badge badge-sm <%= status_css %>"><%= tool_status %></span>
698
311
  <% if tool_duration.present? %>
699
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300">
700
- <%= tool_duration %>ms
701
- </span>
312
+ <span class="badge badge-sm badge-purple"><%= tool_duration %>ms</span>
702
313
  <% end %>
703
314
  </div>
704
- <div class="flex items-center gap-3 text-xs text-gray-400 dark:text-gray-500">
315
+ <div class="flex items-center gap-2 font-mono text-xs text-gray-400 dark:text-gray-600">
705
316
  <% if tool_called_at.present? %>
706
- <span class="font-mono" title="Called at"><%= Time.parse(tool_called_at).strftime("%H:%M:%S.%L") rescue tool_called_at %></span>
317
+ <span title="Called at"><%= Time.parse(tool_called_at).strftime("%H:%M:%S.%L") rescue tool_called_at %></span>
707
318
  <% end %>
708
319
  <% if tool_id.present? %>
709
- <span class="font-mono truncate max-w-[120px]" title="<%= tool_id %>">
710
- <%= tool_id.to_s.truncate(16) %>
711
- </span>
320
+ <span class="truncate max-w-[120px]" title="<%= tool_id %>"><%= tool_id.to_s.truncate(16) %></span>
712
321
  <% end %>
713
322
  </div>
714
323
  </div>
715
324
  </div>
716
325
 
717
- <!-- Tool Call Arguments -->
718
326
  <% if tool_args.present? && tool_args.any? %>
719
- <div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700">
720
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Arguments</p>
721
- <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-3 text-sm overflow-x-auto font-mono"><%= highlight_json(tool_args) %></pre>
327
+ <div class="px-3 py-2 border-t border-gray-100 dark:border-gray-800">
328
+ <p class="text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono mb-1">arguments</p>
329
+ <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded p-2 text-xs overflow-x-auto font-mono"><%= highlight_json(tool_args) %></pre>
722
330
  </div>
723
331
  <% else %>
724
- <div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700">
725
- <p class="text-xs text-gray-400 dark:text-gray-500 italic">No arguments</p>
332
+ <div class="px-3 py-2 border-t border-gray-100 dark:border-gray-800">
333
+ <p class="text-xs text-gray-400 dark:text-gray-600 font-mono italic">no arguments</p>
726
334
  </div>
727
335
  <% end %>
728
336
 
729
- <!-- Tool Call Result (NEW) -->
730
337
  <% if tool_result.present? %>
731
- <div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700">
732
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Result</p>
733
- <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 max-h-48 overflow-y-auto">
734
- <pre class="text-sm text-gray-900 dark:text-gray-100 font-mono whitespace-pre-wrap break-words"><%= tool_result.is_a?(String) ? tool_result : JSON.pretty_generate(tool_result) rescue tool_result.to_s %></pre>
338
+ <div class="px-3 py-2 border-t border-gray-100 dark:border-gray-800">
339
+ <p class="text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono mb-1">result</p>
340
+ <div class="bg-gray-50 dark:bg-gray-900 rounded p-2 max-h-48 overflow-y-auto">
341
+ <pre class="text-xs text-gray-900 dark:text-gray-100 font-mono whitespace-pre-wrap break-words"><%= tool_result.is_a?(String) ? tool_result : JSON.pretty_generate(tool_result) rescue tool_result.to_s %></pre>
735
342
  </div>
736
343
  </div>
737
344
  <% end %>
738
345
 
739
- <!-- Tool Call Error (NEW) -->
740
346
  <% if tool_status == 'error' && tool_error.present? %>
741
- <div class="px-4 py-3 border-t border-red-100 dark:border-red-900/50 bg-red-50 dark:bg-red-900/20">
742
- <p class="text-xs text-red-600 dark:text-red-400 uppercase tracking-wide mb-2">Error</p>
743
- <pre class="text-sm text-red-700 dark:text-red-300 font-mono whitespace-pre-wrap break-words"><%= tool_error %></pre>
347
+ <div class="px-3 py-2 border-t border-red-100 dark:border-red-500/30 bg-red-50 dark:bg-red-500/10">
348
+ <p class="text-[10px] text-red-600 dark:text-red-400 uppercase tracking-wider font-mono mb-1">error</p>
349
+ <pre class="text-xs text-red-700 dark:text-red-300 font-mono whitespace-pre-wrap break-words"><%= tool_error %></pre>
744
350
  </div>
745
351
  <% end %>
746
352
  </div>
747
353
  <% end %>
748
354
  </div>
749
355
  <% else %>
750
- <p class="text-sm text-gray-400 dark:text-gray-500 italic">No tool calls were made during this execution.</p>
356
+ <p class="text-xs text-gray-400 dark:text-gray-600 font-mono italic">no tool calls</p>
751
357
  <% end %>
752
358
  </div>
753
359
 
754
- <!-- Metadata -->
360
+ <!-- ── metadata ──────────────────────── -->
755
361
  <% if @execution.metadata.present? && @execution.metadata.any? %>
756
- <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 }">
757
- <div class="flex items-center justify-between">
758
- <div class="flex items-center gap-2">
759
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Metadata</h3>
760
- <span class="text-xs text-gray-400 dark:text-gray-500">(<%= @execution.metadata.keys.count %> keys)</span>
761
- </div>
762
- <div class="flex items-center gap-2">
763
- <button
764
- type="button"
765
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
766
- 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"
767
- >
768
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
769
- <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"/>
770
- </svg>
771
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
772
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
773
- </svg>
774
- <span>Copy</span>
775
- </button>
776
- <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">
777
- <span x-text="expanded ? 'Collapse' : 'Expand'">Expand</span>
778
- </button>
779
- </div>
362
+ <div x-data="{ expanded: false }">
363
+ <div class="flex items-center gap-3 mt-6 mb-3">
364
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">metadata (<%= @execution.metadata.keys.count %>)</span>
365
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
366
+ <button
367
+ type="button"
368
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
369
+ class="copy-json-btn font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
370
+ >copy</button>
371
+ <button type="button" @click="expanded = !expanded" class="font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
372
+ <span x-text="expanded ? 'collapse' : 'expand'">expand</span>
373
+ </button>
780
374
  </div>
781
- <div x-show="expanded" x-cloak class="mt-4">
782
- <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>
375
+ <div x-show="expanded" x-cloak>
376
+ <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded p-4 text-xs overflow-x-auto font-mono"><%= highlight_json(@execution.metadata) %></pre>
783
377
  </div>
784
378
  </div>
785
379
  <% end %>
786
380
 
787
- <!-- Execution Hierarchy -->
381
+ <!-- ── hierarchy ──────────────────────── -->
788
382
  <% if @execution.parent_execution_id.present? || (@execution.respond_to?(:child_executions) && @execution.child_executions.any?) %>
789
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
790
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-4">Execution Hierarchy</h3>
383
+ <div class="flex items-center gap-3 mt-6 mb-3">
384
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">hierarchy</span>
385
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
386
+ </div>
791
387
 
388
+ <div class="font-mono text-xs space-y-1.5">
792
389
  <% if @execution.parent_execution_id.present? %>
793
- <div class="mb-4">
794
- <span class="text-xs text-gray-500 dark:text-gray-400">Parent Execution:</span>
390
+ <div class="text-gray-400 dark:text-gray-500">
391
+ parent:
795
392
  <%= link_to "##{@execution.parent_execution_id}",
796
393
  ruby_llm_agents.execution_path(@execution.parent_execution_id),
797
- class: "ml-2 text-blue-600 dark:text-blue-400 hover:underline font-mono text-sm" %>
394
+ class: "text-blue-600 dark:text-blue-400 hover:underline" %>
798
395
  </div>
799
396
  <% end %>
800
397
 
801
398
  <% if @execution.respond_to?(:child_executions) && @execution.child_executions.any? %>
802
- <div>
803
- <span class="text-xs text-gray-500 dark:text-gray-400">Child Executions (<%= @execution.child_executions.count %>):</span>
804
- <div class="mt-2 flex flex-wrap gap-2">
399
+ <div class="text-gray-400 dark:text-gray-500">
400
+ children (<%= @execution.child_executions.count %>):
401
+ <span class="inline-flex flex-wrap gap-1.5 ml-1">
805
402
  <% @execution.child_executions.limit(10).each do |child| %>
806
403
  <%= link_to "##{child.id}",
807
404
  ruby_llm_agents.execution_path(child),
808
- class: "inline-flex items-center px-2 py-1 rounded text-xs font-mono bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600" %>
405
+ class: "text-blue-600 dark:text-blue-400 hover:underline" %>
809
406
  <% end %>
810
407
  <% if @execution.child_executions.count > 10 %>
811
- <span class="text-xs text-gray-400">+<%= @execution.child_executions.count - 10 %> more</span>
408
+ <span>+<%= @execution.child_executions.count - 10 %> more</span>
812
409
  <% end %>
813
- </div>
410
+ </span>
814
411
  </div>
815
412
  <% end %>
816
413
  </div>
817
414
  <% end %>
818
415
 
819
- <!-- System Prompt -->
416
+ <!-- ── system prompt ──────────────────── -->
820
417
  <% if @execution.system_prompt.present? %>
821
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
822
- <div class="flex items-center justify-between mb-4">
823
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">System Prompt</h3>
824
- <button type="button" onclick="togglePrompt('system')" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
825
- <span id="system-prompt-toggle">Expand</span>
826
- </button>
827
- </div>
828
- <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>
829
- <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>
418
+ <div class="flex items-center gap-3 mt-6 mb-3">
419
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">system prompt</span>
420
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
421
+ <button type="button" onclick="togglePrompt('system')" class="font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
422
+ <span id="system-prompt-toggle">expand</span>
423
+ </button>
830
424
  </div>
425
+ <p id="system-prompt-preview" class="text-xs text-gray-600 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded p-3 truncate"><%= @execution.system_prompt.truncate(150) %></p>
426
+ <pre id="system-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded p-4 text-xs overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.system_prompt %></pre>
831
427
  <% end %>
832
428
 
833
- <!-- User Prompt -->
429
+ <!-- ── user prompt ──────────────────── -->
834
430
  <% if @execution.user_prompt.present? %>
835
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
836
- <div class="flex items-center justify-between mb-4">
837
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">User Prompt</h3>
838
- <button type="button" onclick="togglePrompt('user')" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
839
- <span id="user-prompt-toggle">Expand</span>
840
- </button>
841
- </div>
842
- <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>
843
- <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>
431
+ <div class="flex items-center gap-3 mt-6 mb-3">
432
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">user prompt</span>
433
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
434
+ <button type="button" onclick="togglePrompt('user')" class="font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
435
+ <span id="user-prompt-toggle">expand</span>
436
+ </button>
844
437
  </div>
438
+ <p id="user-prompt-preview" class="text-xs text-gray-600 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded p-3 truncate"><%= @execution.user_prompt.truncate(150) %></p>
439
+ <pre id="user-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded p-4 text-xs overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.user_prompt %></pre>
845
440
  <% end %>
846
441
 
847
- <!-- Conversation Context -->
442
+ <!-- ── conversation ──────────────────── -->
848
443
  <% if @execution.respond_to?(:messages_count) && @execution.messages_count.to_i > 0 %>
849
444
  <%
850
445
  messages_summary = @execution.messages_summary || {}
851
- # Handle both string and symbol keys
852
446
  first_message = messages_summary["first"] || messages_summary[:first]
853
447
  last_message = messages_summary["last"] || messages_summary[:last]
854
448
  max_len = RubyLLM::Agents.configuration.messages_summary_max_length || 500
855
449
  %>
856
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
857
- <div class="flex items-center gap-2 mb-4">
858
- <svg class="w-5 h-5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
859
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z"/>
860
- </svg>
861
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Conversation Context</h3>
862
- <span class="inline-flex items-center px-2 py-0.5 rounded-full text-xs font-medium bg-indigo-100 dark:bg-indigo-900/50 text-indigo-800 dark:text-indigo-300">
863
- <%= @execution.messages_count %> message<%= @execution.messages_count == 1 ? '' : 's' %>
864
- </span>
865
- </div>
450
+ <div class="flex items-center gap-3 mt-6 mb-3">
451
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">conversation (<%= @execution.messages_count %>)</span>
452
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
453
+ </div>
866
454
 
867
- <div class="space-y-4">
868
- <% if first_message %>
869
- <%
870
- first_role = first_message["role"] || first_message[:role] || "unknown"
871
- first_content = first_message["content"] || first_message[:content] || ""
872
- first_truncated = first_content.length >= max_len
873
- role_badge_class = case first_role.to_s
874
- when "user" then "bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300"
875
- when "assistant" then "bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300"
876
- when "system" then "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300"
877
- else "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300"
878
- end
879
- %>
880
- <div class="border border-gray-100 dark:border-gray-700 rounded-lg p-4">
881
- <div class="flex items-center gap-2 mb-2">
882
- <span class="text-xs text-gray-500 dark:text-gray-400 font-medium">First Message</span>
883
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= role_badge_class %>">
884
- <%= first_role %>
885
- </span>
886
- <% if first_truncated %>
887
- <span class="text-xs text-gray-400 dark:text-gray-500 italic">(truncated)</span>
888
- <% end %>
889
- </div>
890
- <p class="text-sm text-gray-700 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded-lg p-3 whitespace-pre-wrap break-words"><%= first_content %></p>
455
+ <div class="space-y-3">
456
+ <% if first_message %>
457
+ <%
458
+ first_role = first_message["role"] || first_message[:role] || "unknown"
459
+ first_content = first_message["content"] || first_message[:content] || ""
460
+ first_truncated = first_content.length >= max_len
461
+ role_css = case first_role.to_s
462
+ when "user" then "badge-cyan"
463
+ when "assistant" then "badge-success"
464
+ when "system" then "badge-timeout"
465
+ else "badge-timeout"
466
+ end
467
+ %>
468
+ <div>
469
+ <div class="flex items-center gap-2 mb-1 font-mono text-xs">
470
+ <span class="text-gray-400 dark:text-gray-600">first</span>
471
+ <span class="badge badge-sm <%= role_css %>"><%= first_role %></span>
472
+ <% if first_truncated %>
473
+ <span class="text-gray-400 dark:text-gray-600 italic">(truncated)</span>
474
+ <% end %>
891
475
  </div>
892
- <% end %>
476
+ <p class="text-xs text-gray-700 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded p-3 whitespace-pre-wrap break-words"><%= first_content %></p>
477
+ </div>
478
+ <% end %>
893
479
 
894
- <% if last_message %>
895
- <%
896
- last_role = last_message["role"] || last_message[:role] || "unknown"
897
- last_content = last_message["content"] || last_message[:content] || ""
898
- last_truncated = last_content.length >= max_len
899
- role_badge_class = case last_role.to_s
900
- when "user" then "bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300"
901
- when "assistant" then "bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300"
902
- when "system" then "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300"
903
- else "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300"
904
- end
905
- %>
906
- <div class="border border-gray-100 dark:border-gray-700 rounded-lg p-4">
907
- <div class="flex items-center gap-2 mb-2">
908
- <span class="text-xs text-gray-500 dark:text-gray-400 font-medium">Last Message</span>
909
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= role_badge_class %>">
910
- <%= last_role %>
911
- </span>
912
- <% if last_truncated %>
913
- <span class="text-xs text-gray-400 dark:text-gray-500 italic">(truncated)</span>
914
- <% end %>
915
- </div>
916
- <p class="text-sm text-gray-700 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded-lg p-3 whitespace-pre-wrap break-words"><%= last_content %></p>
480
+ <% if last_message %>
481
+ <%
482
+ last_role = last_message["role"] || last_message[:role] || "unknown"
483
+ last_content = last_message["content"] || last_message[:content] || ""
484
+ last_truncated = last_content.length >= max_len
485
+ role_css = case last_role.to_s
486
+ when "user" then "badge-cyan"
487
+ when "assistant" then "badge-success"
488
+ when "system" then "badge-timeout"
489
+ else "badge-timeout"
490
+ end
491
+ %>
492
+ <div>
493
+ <div class="flex items-center gap-2 mb-1 font-mono text-xs">
494
+ <span class="text-gray-400 dark:text-gray-600">last</span>
495
+ <span class="badge badge-sm <%= role_css %>"><%= last_role %></span>
496
+ <% if last_truncated %>
497
+ <span class="text-gray-400 dark:text-gray-600 italic">(truncated)</span>
498
+ <% end %>
917
499
  </div>
918
- <% end %>
500
+ <p class="text-xs text-gray-700 dark:text-gray-300 font-mono bg-gray-50 dark:bg-gray-900 rounded p-3 whitespace-pre-wrap break-words"><%= last_content %></p>
501
+ </div>
502
+ <% end %>
919
503
 
920
- <% if @execution.messages_count > 2 %>
921
- <p class="text-xs text-gray-400 dark:text-gray-500 text-center">
922
- + <%= @execution.messages_count - 2 %> more message<%= @execution.messages_count - 2 == 1 ? '' : 's' %> in between
923
- </p>
924
- <% end %>
925
- </div>
504
+ <% if @execution.messages_count > 2 %>
505
+ <p class="text-xs text-gray-400 dark:text-gray-600 font-mono text-center">
506
+ + <%= @execution.messages_count - 2 %> more message<%= @execution.messages_count - 2 == 1 ? '' : 's' %> in between
507
+ </p>
508
+ <% end %>
926
509
  </div>
927
510
  <% end %>
928
511
 
929
- <!-- Diagnostics Panel -->
930
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
931
- <div class="flex items-center justify-between mb-4">
932
- <div class="flex items-center gap-2">
933
- <svg class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
934
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
935
- </svg>
936
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Diagnostics</h3>
937
- </div>
938
- <button
939
- type="button"
940
- id="toggle-diagnostics-btn"
941
- onclick="toggleDiagnostics()"
942
- class="inline-flex items-center gap-1 px-2 py-1 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 transition-colors"
943
- >
944
- <svg id="diagnostics-expand-icon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
945
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
946
- </svg>
947
- <span id="diagnostics-toggle-text">Expand</span>
512
+ <!-- ── diagnostics ──────────────────── -->
513
+ <div x-data="{ expanded: localStorage.getItem('ruby_llm_agents_diagnostics_expanded') !== 'false' }">
514
+ <div class="flex items-center gap-3 mt-6 mb-3">
515
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">diagnostics</span>
516
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
517
+ <button type="button"
518
+ @click="expanded = !expanded; localStorage.setItem('ruby_llm_agents_diagnostics_expanded', expanded)"
519
+ class="font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300">
520
+ <span x-text="expanded ? 'collapse' : 'expand'">expand</span>
948
521
  </button>
949
522
  </div>
950
523
 
951
- <!-- Quick Info (always visible) -->
952
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
953
- <div>
954
- <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Model</span>
955
- <p class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.model_id %></p>
956
- </div>
524
+ <!-- Quick info (always visible) -->
525
+ <div class="flex flex-wrap items-center gap-x-4 gap-y-1 font-mono text-xs text-gray-400 dark:text-gray-500">
526
+ <span><span class="text-gray-800 dark:text-gray-200"><%= @execution.model_id %></span></span>
527
+ <% if @execution.temperature %>
528
+ <span>temp <span class="text-gray-800 dark:text-gray-200"><%= @execution.temperature %></span></span>
529
+ <% end %>
530
+ <span><span class="text-gray-800 dark:text-gray-200"><%= @execution.status %></span></span>
531
+ </div>
532
+
533
+ <!-- Expanded details -->
534
+ <div x-show="expanded" x-cloak class="mt-4 space-y-4">
535
+ <!-- Timing -->
957
536
  <div>
958
- <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Temperature</span>
959
- <p class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.temperature || 'N/A' %></p>
537
+ <p class="text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono mb-1.5">timing</p>
538
+ <dl class="font-mono text-xs space-y-1">
539
+ <div class="flex justify-between">
540
+ <dt class="text-gray-400 dark:text-gray-500">started</dt>
541
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.started_at&.strftime("%Y-%m-%d %H:%M:%S.%L") || 'N/A' %></dd>
542
+ </div>
543
+ <div class="flex justify-between">
544
+ <dt class="text-gray-400 dark:text-gray-500">completed</dt>
545
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.completed_at&.strftime("%Y-%m-%d %H:%M:%S.%L") || 'N/A' %></dd>
546
+ </div>
547
+ <div class="flex justify-between">
548
+ <dt class="text-gray-400 dark:text-gray-500">duration</dt>
549
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.duration_ms ? "#{@execution.duration_ms}ms" : 'N/A' %></dd>
550
+ </div>
551
+ <% if @execution.streaming? && @execution.time_to_first_token_ms %>
552
+ <div class="flex justify-between">
553
+ <dt class="text-gray-400 dark:text-gray-500">time to first token</dt>
554
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.time_to_first_token_ms %>ms</dd>
555
+ </div>
556
+ <% end %>
557
+ <div class="flex justify-between">
558
+ <dt class="text-gray-400 dark:text-gray-500">created at</dt>
559
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
560
+ </div>
561
+ </dl>
960
562
  </div>
563
+
564
+ <!-- Performance -->
961
565
  <div>
962
- <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Version</span>
963
- <p class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.agent_version || '1.0' %></p>
566
+ <p class="text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono mb-1.5">performance</p>
567
+ <dl class="font-mono text-xs space-y-1">
568
+ <div class="flex justify-between">
569
+ <dt class="text-gray-400 dark:text-gray-500">tokens/second</dt>
570
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.tokens_per_second&.round(1) || 'N/A' %></dd>
571
+ </div>
572
+ <div class="flex justify-between">
573
+ <dt class="text-gray-400 dark:text-gray-500">input tokens</dt>
574
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.input_tokens || 0 %></dd>
575
+ </div>
576
+ <div class="flex justify-between">
577
+ <dt class="text-gray-400 dark:text-gray-500">output tokens</dt>
578
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.output_tokens || 0 %></dd>
579
+ </div>
580
+ <div class="flex justify-between">
581
+ <dt class="text-gray-400 dark:text-gray-500">cached tokens</dt>
582
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.cached_tokens || 0 %></dd>
583
+ </div>
584
+ </dl>
964
585
  </div>
586
+
587
+ <!-- Cost -->
965
588
  <div>
966
- <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Status</span>
967
- <p class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.status %></p>
589
+ <p class="text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono mb-1.5">cost</p>
590
+ <dl class="font-mono text-xs space-y-1">
591
+ <div class="flex justify-between">
592
+ <dt class="text-gray-400 dark:text-gray-500">input cost</dt>
593
+ <dd class="text-gray-900 dark:text-gray-100">$<%= number_with_precision(@execution.input_cost || 0, precision: 6) %></dd>
594
+ </div>
595
+ <div class="flex justify-between">
596
+ <dt class="text-gray-400 dark:text-gray-500">output cost</dt>
597
+ <dd class="text-gray-900 dark:text-gray-100">$<%= number_with_precision(@execution.output_cost || 0, precision: 6) %></dd>
598
+ </div>
599
+ <div class="flex justify-between">
600
+ <dt class="text-gray-400 dark:text-gray-500">total cost</dt>
601
+ <dd class="text-gray-800 dark:text-gray-200 font-semibold">$<%= number_with_precision(@execution.total_cost || 0, precision: 6) %></dd>
602
+ </div>
603
+ </dl>
968
604
  </div>
969
- </div>
970
605
 
971
- <!-- Expanded Details (hidden by default) -->
972
- <div id="diagnostics-details" class="hidden mt-6 pt-6 border-t border-gray-100 dark:border-gray-700">
973
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
974
- <!-- Timing Information -->
975
- <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
976
- <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Timing</h4>
977
- <dl class="space-y-2 text-sm">
978
- <div class="flex justify-between">
979
- <dt class="text-gray-500 dark:text-gray-400">Started</dt>
980
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.started_at&.strftime("%Y-%m-%d %H:%M:%S.%L") || 'N/A' %></dd>
981
- </div>
606
+ <!-- Configuration -->
607
+ <div>
608
+ <p class="text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono mb-1.5">configuration</p>
609
+ <dl class="font-mono text-xs space-y-1">
610
+ <div class="flex justify-between">
611
+ <dt class="text-gray-400 dark:text-gray-500">agent type</dt>
612
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.agent_type %></dd>
613
+ </div>
614
+ <div class="flex justify-between">
615
+ <dt class="text-gray-400 dark:text-gray-500">temperature</dt>
616
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.temperature || 'default' %></dd>
617
+ </div>
618
+ <% if @execution.respond_to?(:chosen_model_id) && @execution.chosen_model_id.present? && @execution.chosen_model_id != @execution.model_id %>
982
619
  <div class="flex justify-between">
983
- <dt class="text-gray-500 dark:text-gray-400">Completed</dt>
984
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.completed_at&.strftime("%Y-%m-%d %H:%M:%S.%L") || 'N/A' %></dd>
620
+ <dt class="text-gray-400 dark:text-gray-500">chosen model</dt>
621
+ <dd class="text-amber-600 dark:text-amber-400"><%= @execution.chosen_model_id %></dd>
985
622
  </div>
623
+ <% end %>
624
+ <% if @execution.respond_to?(:attempts_count) && @execution.attempts_count && @execution.attempts_count > 1 %>
986
625
  <div class="flex justify-between">
987
- <dt class="text-gray-500 dark:text-gray-400">Duration</dt>
988
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.duration_ms ? "#{@execution.duration_ms}ms" : 'N/A' %></dd>
626
+ <dt class="text-gray-400 dark:text-gray-500">retry count</dt>
627
+ <dd class="text-blue-600 dark:text-blue-400"><%= @execution.attempts_count - 1 %></dd>
989
628
  </div>
990
- <% if @execution.streaming? && @execution.time_to_first_token_ms %>
629
+ <% end %>
630
+ </dl>
631
+ </div>
632
+
633
+ <!-- Tracing -->
634
+ <% if @execution.trace_id.present? || @execution.request_id.present? %>
635
+ <div>
636
+ <p class="text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono mb-1.5">tracing</p>
637
+ <dl class="font-mono text-xs space-y-1">
638
+ <% if @execution.request_id.present? %>
991
639
  <div class="flex justify-between">
992
- <dt class="text-gray-500 dark:text-gray-400">Time to First Token</dt>
993
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.time_to_first_token_ms %>ms</dd>
640
+ <dt class="text-gray-400 dark:text-gray-500">request id</dt>
641
+ <dd class="text-gray-900 dark:text-gray-100 text-[11px]"><%= @execution.request_id %></dd>
642
+ </div>
643
+ <% end %>
644
+ <% if @execution.trace_id.present? %>
645
+ <div class="flex justify-between">
646
+ <dt class="text-gray-400 dark:text-gray-500">trace id</dt>
647
+ <dd class="text-gray-900 dark:text-gray-100 text-[11px]"><%= @execution.trace_id %></dd>
648
+ </div>
649
+ <% end %>
650
+ <% if @execution.span_id.present? %>
651
+ <div class="flex justify-between">
652
+ <dt class="text-gray-400 dark:text-gray-500">span id</dt>
653
+ <dd class="text-gray-900 dark:text-gray-100 text-[11px]"><%= @execution.span_id %></dd>
994
654
  </div>
995
655
  <% end %>
996
- <div class="flex justify-between">
997
- <dt class="text-gray-500 dark:text-gray-400">Created At</dt>
998
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
999
- </div>
1000
- </dl>
1001
- </div>
1002
-
1003
- <!-- Performance Metrics -->
1004
- <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
1005
- <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Performance</h4>
1006
- <dl class="space-y-2 text-sm">
1007
- <div class="flex justify-between">
1008
- <dt class="text-gray-500 dark:text-gray-400">Tokens/Second</dt>
1009
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.tokens_per_second&.round(1) || 'N/A' %></dd>
1010
- </div>
1011
- <div class="flex justify-between">
1012
- <dt class="text-gray-500 dark:text-gray-400">Input Tokens</dt>
1013
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.input_tokens || 0 %></dd>
1014
- </div>
1015
- <div class="flex justify-between">
1016
- <dt class="text-gray-500 dark:text-gray-400">Output Tokens</dt>
1017
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.output_tokens || 0 %></dd>
1018
- </div>
1019
- <div class="flex justify-between">
1020
- <dt class="text-gray-500 dark:text-gray-400">Cached Tokens</dt>
1021
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.cached_tokens || 0 %></dd>
1022
- </div>
1023
- </dl>
1024
- </div>
1025
-
1026
- <!-- Cost Breakdown -->
1027
- <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
1028
- <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Cost</h4>
1029
- <dl class="space-y-2 text-sm">
1030
- <div class="flex justify-between">
1031
- <dt class="text-gray-500 dark:text-gray-400">Input Cost</dt>
1032
- <dd class="font-mono text-gray-900 dark:text-gray-100">$<%= number_with_precision(@execution.input_cost || 0, precision: 6) %></dd>
1033
- </div>
1034
- <div class="flex justify-between">
1035
- <dt class="text-gray-500 dark:text-gray-400">Output Cost</dt>
1036
- <dd class="font-mono text-gray-900 dark:text-gray-100">$<%= number_with_precision(@execution.output_cost || 0, precision: 6) %></dd>
1037
- </div>
1038
- <div class="flex justify-between font-semibold">
1039
- <dt class="text-gray-600 dark:text-gray-300">Total Cost</dt>
1040
- <dd class="font-mono text-gray-900 dark:text-gray-100">$<%= number_with_precision(@execution.total_cost || 0, precision: 6) %></dd>
1041
- </div>
1042
656
  </dl>
1043
657
  </div>
658
+ <% end %>
1044
659
 
1045
- <!-- Configuration -->
1046
- <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
1047
- <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Configuration</h4>
1048
- <dl class="space-y-2 text-sm">
1049
- <div class="flex justify-between">
1050
- <dt class="text-gray-500 dark:text-gray-400">Agent Type</dt>
1051
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.agent_type %></dd>
1052
- </div>
660
+ <!-- Caching -->
661
+ <% if @execution.cache_hit || @execution.response_cache_key.present? %>
662
+ <div>
663
+ <p class="text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-wider font-mono mb-1.5">caching</p>
664
+ <dl class="font-mono text-xs space-y-1">
1053
665
  <div class="flex justify-between">
1054
- <dt class="text-gray-500 dark:text-gray-400">Temperature</dt>
1055
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.temperature || 'default' %></dd>
666
+ <dt class="text-gray-400 dark:text-gray-500">cache hit</dt>
667
+ <dd class="<%= @execution.cache_hit ? 'text-green-600 dark:text-green-400' : 'text-gray-400' %>"><%= @execution.cache_hit ? 'yes' : 'no' %></dd>
1056
668
  </div>
1057
- <% if @execution.respond_to?(:chosen_model_id) && @execution.chosen_model_id.present? && @execution.chosen_model_id != @execution.model_id %>
669
+ <% if @execution.response_cache_key.present? %>
1058
670
  <div class="flex justify-between">
1059
- <dt class="text-gray-500 dark:text-gray-400">Chosen Model</dt>
1060
- <dd class="font-mono text-amber-600 dark:text-amber-400"><%= @execution.chosen_model_id %></dd>
671
+ <dt class="text-gray-400 dark:text-gray-500">cache key</dt>
672
+ <dd class="text-gray-900 dark:text-gray-100 text-[11px] truncate max-w-xs" title="<%= @execution.response_cache_key %>"><%= @execution.response_cache_key.truncate(30) %></dd>
1061
673
  </div>
1062
674
  <% end %>
1063
- <% if @execution.respond_to?(:attempts_count) && @execution.attempts_count && @execution.attempts_count > 1 %>
675
+ <% if @execution.cached_at.present? %>
1064
676
  <div class="flex justify-between">
1065
- <dt class="text-gray-500 dark:text-gray-400">Retry Count</dt>
1066
- <dd class="font-mono text-blue-600 dark:text-blue-400"><%= @execution.attempts_count - 1 %></dd>
677
+ <dt class="text-gray-400 dark:text-gray-500">cached at</dt>
678
+ <dd class="text-gray-900 dark:text-gray-100"><%= @execution.cached_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
1067
679
  </div>
1068
680
  <% end %>
1069
681
  </dl>
1070
682
  </div>
1071
- </div>
1072
-
1073
- <!-- Second Row: Tracing and Caching -->
1074
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
1075
- <!-- Tracing Information -->
1076
- <% if @execution.trace_id.present? || @execution.request_id.present? %>
1077
- <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
1078
- <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Tracing</h4>
1079
- <dl class="space-y-2 text-sm">
1080
- <% if @execution.request_id.present? %>
1081
- <div class="flex justify-between">
1082
- <dt class="text-gray-500 dark:text-gray-400">Request ID</dt>
1083
- <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs"><%= @execution.request_id %></dd>
1084
- </div>
1085
- <% end %>
1086
- <% if @execution.trace_id.present? %>
1087
- <div class="flex justify-between">
1088
- <dt class="text-gray-500 dark:text-gray-400">Trace ID</dt>
1089
- <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs"><%= @execution.trace_id %></dd>
1090
- </div>
1091
- <% end %>
1092
- <% if @execution.span_id.present? %>
1093
- <div class="flex justify-between">
1094
- <dt class="text-gray-500 dark:text-gray-400">Span ID</dt>
1095
- <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs"><%= @execution.span_id %></dd>
1096
- </div>
1097
- <% end %>
1098
- </dl>
1099
- </div>
1100
- <% end %>
1101
-
1102
- <!-- Caching Information -->
1103
- <% if @execution.cache_hit || @execution.response_cache_key.present? %>
1104
- <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
1105
- <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Caching</h4>
1106
- <dl class="space-y-2 text-sm">
1107
- <div class="flex justify-between">
1108
- <dt class="text-gray-500 dark:text-gray-400">Cache Hit</dt>
1109
- <dd class="font-mono text-gray-900 dark:text-gray-100">
1110
- <% if @execution.cache_hit %>
1111
- <span class="text-green-600 dark:text-green-400">Yes</span>
1112
- <% else %>
1113
- <span class="text-gray-400">No</span>
1114
- <% end %>
1115
- </dd>
1116
- </div>
1117
- <% if @execution.response_cache_key.present? %>
1118
- <div class="flex justify-between">
1119
- <dt class="text-gray-500 dark:text-gray-400">Cache Key</dt>
1120
- <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs truncate max-w-xs" title="<%= @execution.response_cache_key %>">
1121
- <%= @execution.response_cache_key.truncate(30) %>
1122
- </dd>
1123
- </div>
1124
- <% end %>
1125
- <% if @execution.cached_at.present? %>
1126
- <div class="flex justify-between">
1127
- <dt class="text-gray-500 dark:text-gray-400">Cached At</dt>
1128
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.cached_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
1129
- </div>
1130
- <% end %>
1131
- </dl>
1132
- </div>
1133
- <% end %>
1134
- </div>
683
+ <% end %>
1135
684
 
1136
- <!-- Execution ID -->
1137
- <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
1138
- <div class="flex items-center justify-between">
1139
- <span class="text-xs text-gray-500 dark:text-gray-400">
1140
- Execution ID: <code class="bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded font-mono"><%= @execution.id %></code>
1141
- </span>
1142
- <button
1143
- type="button"
1144
- onclick="copyDiagnostics()"
1145
- class="inline-flex items-center gap-1 px-2 py-1 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 transition-colors"
1146
- >
1147
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1148
- <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"/>
1149
- </svg>
1150
- Copy All
1151
- </button>
1152
- </div>
685
+ <!-- Footer: id + copy all -->
686
+ <div class="flex items-center justify-between pt-3 border-t border-gray-200 dark:border-gray-800">
687
+ <span class="font-mono text-xs text-gray-400 dark:text-gray-500">
688
+ id: <span class="text-gray-800 dark:text-gray-200"><%= @execution.id %></span>
689
+ </span>
690
+ <button
691
+ type="button"
692
+ onclick="copyDiagnostics()"
693
+ class="font-mono text-xs text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300"
694
+ >copy all</button>
1153
695
  </div>
1154
696
  </div>
1155
697
  </div>
@@ -1163,88 +705,36 @@
1163
705
  const isHidden = content.classList.contains('hidden');
1164
706
  content.classList.toggle('hidden', !isHidden);
1165
707
  if (preview) preview.classList.toggle('hidden', isHidden);
1166
- toggle.textContent = isHidden ? 'Collapse' : 'Expand';
708
+ toggle.textContent = isHidden ? 'collapse' : 'expand';
1167
709
  }
1168
710
 
1169
711
  document.addEventListener('DOMContentLoaded', function() {
1170
712
  document.querySelectorAll('.copy-json-btn').forEach(function(button) {
1171
713
  button.addEventListener('click', function() {
1172
714
  const jsonText = atob(this.getAttribute('data-copy-json'));
1173
- const span = this.querySelector('span');
1174
- const copyIcon = this.querySelector('.copy-icon');
1175
- const checkIcon = this.querySelector('.check-icon');
715
+ const btn = this;
716
+ const originalText = btn.textContent;
1176
717
 
1177
718
  navigator.clipboard.writeText(jsonText).then(function() {
1178
- span.textContent = 'Copied!';
1179
- copyIcon.classList.add('hidden');
1180
- checkIcon.classList.remove('hidden');
1181
- button.classList.add('text-green-600');
1182
-
719
+ btn.textContent = 'copied!';
1183
720
  setTimeout(function() {
1184
- span.textContent = 'Copy';
1185
- copyIcon.classList.remove('hidden');
1186
- checkIcon.classList.add('hidden');
1187
- button.classList.remove('text-green-600');
721
+ btn.textContent = originalText;
1188
722
  }, 2000);
1189
723
  }).catch(function(err) {
1190
724
  console.error('Failed to copy:', err);
1191
- span.textContent = 'Failed';
725
+ btn.textContent = 'failed';
1192
726
  setTimeout(function() {
1193
- span.textContent = 'Copy';
727
+ btn.textContent = originalText;
1194
728
  }, 2000);
1195
729
  });
1196
730
  });
1197
731
  });
1198
732
  });
1199
733
 
1200
- // Rerun modal functions
1201
- function confirmRerun() {
1202
- document.getElementById('rerun-modal').classList.remove('hidden');
1203
- document.body.classList.add('overflow-hidden');
1204
- }
1205
-
1206
- function closeRerunModal() {
1207
- document.getElementById('rerun-modal').classList.add('hidden');
1208
- document.body.classList.remove('overflow-hidden');
1209
- }
1210
-
1211
- // Close modal on Escape key
1212
- document.addEventListener('keydown', function(e) {
1213
- if (e.key === 'Escape') {
1214
- closeRerunModal();
1215
- }
1216
- });
1217
-
1218
- // Diagnostics panel toggle (default to expanded)
1219
- let diagnosticsExpanded = localStorage.getItem('ruby_llm_agents_diagnostics_expanded') !== 'false';
1220
-
1221
- function toggleDiagnostics() {
1222
- diagnosticsExpanded = !diagnosticsExpanded;
1223
- localStorage.setItem('ruby_llm_agents_diagnostics_expanded', diagnosticsExpanded);
1224
- updateDiagnosticsUI();
1225
- }
1226
-
1227
- function updateDiagnosticsUI() {
1228
- const details = document.getElementById('diagnostics-details');
1229
- const toggleText = document.getElementById('diagnostics-toggle-text');
1230
- const expandIcon = document.getElementById('diagnostics-expand-icon');
1231
-
1232
- if (diagnosticsExpanded) {
1233
- details.classList.remove('hidden');
1234
- toggleText.textContent = 'Collapse';
1235
- expandIcon.style.transform = 'rotate(180deg)';
1236
- } else {
1237
- details.classList.add('hidden');
1238
- toggleText.textContent = 'Expand';
1239
- expandIcon.style.transform = 'rotate(0deg)';
1240
- }
1241
- }
1242
-
1243
734
  function copyDiagnostics() {
1244
735
  const diagnostics = {
1245
736
  execution_id: <%= @execution.id %>,
1246
737
  agent_type: "<%= @execution.agent_type %>",
1247
- agent_version: "<%= @execution.agent_version || '1.0' %>",
1248
738
  model_id: "<%= @execution.model_id %>",
1249
739
  status: "<%= @execution.status %>",
1250
740
  temperature: <%= @execution.temperature || 'null' %>,
@@ -1261,16 +751,15 @@
1261
751
  created_at: "<%= @execution.created_at.iso8601 %>"
1262
752
  };
1263
753
 
754
+ const btn = event.currentTarget;
1264
755
  navigator.clipboard.writeText(JSON.stringify(diagnostics, null, 2)).then(function() {
1265
- alert('Diagnostics copied to clipboard!');
756
+ btn.textContent = 'copied!';
757
+ setTimeout(function() {
758
+ btn.textContent = 'copy all';
759
+ }, 2000);
1266
760
  }).catch(function(err) {
1267
761
  console.error('Failed to copy diagnostics:', err);
1268
762
  });
1269
763
  }
1270
-
1271
- // Initialize diagnostics panel on page load
1272
- document.addEventListener('DOMContentLoaded', function() {
1273
- updateDiagnosticsUI();
1274
- });
1275
764
  </script>
1276
765
  </div>