ruby_llm-agents 1.3.3 → 2.0.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -334
  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 +46 -10
  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 +87 -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 +528 -989
  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 +9 -14
  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 +9 -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 +58 -262
  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 +52 -6
  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 +58 -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/attempt_tracker.rb +1 -0
  99. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  100. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  101. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  102. data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
  103. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  104. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  105. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  106. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  107. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  108. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  109. data/lib/ruby_llm/agents/results/base.rb +1 -49
  110. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  111. data/lib/ruby_llm/agents.rb +1 -9
  112. data/lib/tasks/ruby_llm_agents.rake +34 -0
  113. metadata +12 -81
  114. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  115. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  116. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  117. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  118. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  119. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  120. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  121. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  122. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  123. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  125. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  126. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  127. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  128. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  129. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  130. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  131. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  132. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  133. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  134. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  135. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  136. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  137. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  138. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  139. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  140. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  141. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  142. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  143. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  144. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  145. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  146. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  147. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  148. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  149. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  150. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  151. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  152. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  153. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  154. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  155. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  156. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  157. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  158. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  159. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  160. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  161. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  162. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  163. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  164. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  165. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  166. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  167. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  168. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  169. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  170. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  171. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  172. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  173. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  174. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  175. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  176. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  177. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  178. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  180. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  181. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  182. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  183. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  185. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  186. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  187. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  188. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  189. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  190. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  191. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  192. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,622 +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
- <div class="overflow-x-auto">
402
- <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
403
- <thead>
404
- <tr>
405
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">#</th>
406
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Model</th>
407
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
408
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Duration</th>
409
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tokens</th>
410
- <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Error</th>
411
- </tr>
412
- </thead>
413
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
414
- <% @execution.attempts.each_with_index do |attempt, index| %>
415
- <tr class="<%= attempt['short_circuited'] ? 'bg-gray-50 dark:bg-gray-900/50' : '' %>">
416
- <td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
417
- <%= index + 1 %>
418
- </td>
419
- <td class="px-3 py-2 text-sm font-mono text-gray-900 dark:text-gray-100">
420
- <%= attempt['model_id'] %>
421
- </td>
422
- <td class="px-3 py-2">
423
- <% if attempt['short_circuited'] %>
424
- <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">
425
- <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
426
- <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"/>
427
- </svg>
428
- Blocked
429
- </span>
430
- <% elsif attempt['error_class'].present? %>
431
- <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">
432
- <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
433
- <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"/>
434
- </svg>
435
- Failed
436
- </span>
437
- <% else %>
438
- <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">
439
- <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
440
- <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"/>
441
- </svg>
442
- Success
443
- </span>
444
- <% end %>
445
- </td>
446
- <td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
447
- <%= attempt['duration_ms'] ? "#{attempt['duration_ms']}ms" : '-' %>
448
- </td>
449
- <td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
450
- <% if attempt['input_tokens'] || attempt['output_tokens'] %>
451
- <span class="text-blue-600 dark:text-blue-400"><%= attempt['input_tokens'] || 0 %></span>
452
- /
453
- <span class="text-green-600 dark:text-green-400"><%= attempt['output_tokens'] || 0 %></span>
454
- <% else %>
455
- -
456
- <% end %>
457
- </td>
458
- <td class="px-3 py-2 text-sm">
459
- <% if attempt['error_class'].present? %>
460
- <span class="text-red-600 dark:text-red-400 font-mono text-xs" title="<%= attempt['error_message'] %>">
461
- <%= attempt['error_class'].split('::').last.truncate(30) %>
462
- </span>
463
- <% else %>
464
- <span class="text-gray-400">-</span>
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 %>
122
+
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>
465
170
  <% end %>
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 %>
189
+ </div>
190
+ </div>
466
191
  </td>
467
192
  </tr>
468
193
  <% end %>
469
- </tbody>
470
- </table>
471
- </div>
472
-
473
- <% if @execution.fallback_chain.present? && @execution.fallback_chain.any? %>
474
- <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
475
- <p class="text-xs text-gray-500 dark:text-gray-400">
476
- <span class="font-medium">Fallback chain:</span>
477
- <%= @execution.fallback_chain.join(' → ') %>
478
- </p>
479
- <% if @execution.fallback_reason.present? %>
480
- <p class="text-xs text-amber-600 dark:text-amber-400 mt-1">
481
- Fallback reason: <%= @execution.fallback_reason %>
482
- </p>
483
194
  <% end %>
484
- </div>
485
- <% end %>
195
+ </tbody>
196
+ </table>
486
197
  </div>
487
- <% end %>
488
198
 
489
- <% if @execution.status_error? %>
490
- <!-- Error Details -->
491
- <div class="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg p-6 mb-6">
492
- <div class="flex items-center justify-between mb-2">
493
- <h3 class="text-lg font-semibold text-red-800 dark:text-red-300">Error Details</h3>
494
- <div class="flex items-center gap-2">
495
- <% if @execution.rate_limited? %>
496
- <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">
497
- Rate Limited
498
- </span>
499
- <% end %>
500
- <% if @execution.retryable %>
501
- <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">
502
- Retryable
503
- </span>
504
- <% end %>
505
- <button
506
- type="button"
507
- data-copy-json="<%= Base64.strict_encode64({
508
- error_class: @execution.error_class,
509
- error_message: @execution.error_message
510
- }.to_json) %>"
511
- 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"
512
- >
513
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
514
- <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"/>
515
- </svg>
516
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
517
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
518
- </svg>
519
- <span>Copy</span>
520
- </button>
521
- </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 %>
522
207
  </div>
523
-
524
- <p class="font-mono text-sm text-red-700 dark:text-red-400 mb-2">
525
- <%= @execution.error_class %>
526
- </p>
527
-
528
- <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>
529
- </div>
208
+ <% end %>
530
209
  <% end %>
531
210
 
532
- <!-- Parameters -->
533
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
534
- <div class="flex items-center justify-between mb-4">
535
- <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>
536
224
  <button
537
225
  type="button"
538
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.parameters || {})) %>"
539
- 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"
540
- >
541
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
542
- <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"/>
543
- </svg>
544
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
545
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
546
- </svg>
547
- <span>Copy</span>
548
- </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>
549
232
  </div>
550
- <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>
551
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>
552
249
 
553
- <!-- Response -->
250
+ <!-- ── response ──────────────────────── -->
554
251
  <% if @execution.response.present? %>
555
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
556
- <div class="flex items-center justify-between mb-4">
557
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Response</h3>
558
- <button
559
- type="button"
560
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.response)) %>"
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>
572
- <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>
573
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>
574
262
  <% end %>
575
263
 
576
- <!-- Tool Calls -->
264
+ <!-- ── tool calls ──────────────────── -->
577
265
  <% tool_calls = @execution.tool_calls || [] %>
578
266
  <% tool_call_count = tool_calls.size %>
579
- <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 %> }">
580
- <div class="flex items-center justify-between mb-4">
581
- <div class="flex items-center gap-2">
582
- <svg class="w-5 h-5 text-blue-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
583
- <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"/>
584
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
585
- </svg>
586
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Tool Calls</h3>
587
- <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">
588
- <%= tool_call_count %>
589
- </span>
590
- </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>
591
271
  <% if tool_call_count > 0 %>
592
- <div class="flex items-center gap-2">
593
- <button
594
- type="button"
595
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(tool_calls)) %>"
596
- 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"
597
- >
598
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
599
- <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"/>
600
- </svg>
601
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
602
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
603
- </svg>
604
- <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>
605
280
  </button>
606
- <% if tool_call_count > 3 %>
607
- <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">
608
- <span x-text="expanded ? 'Collapse' : 'Expand'">Expand</span>
609
- </button>
610
- <% end %>
611
- </div>
281
+ <% end %>
612
282
  <% end %>
613
283
  </div>
614
284
 
615
285
  <% if tool_call_count > 0 %>
616
- <div class="space-y-4" x-show="expanded" x-cloak>
286
+ <div class="space-y-3" x-show="expanded" x-cloak>
617
287
  <% tool_calls.each_with_index do |tool_call, index| %>
618
288
  <%
619
- # Handle both symbol and string keys for backward compatibility
620
289
  tool_id = tool_call['id'] || tool_call[:id]
621
290
  tool_name = tool_call['name'] || tool_call[:name]
622
291
  tool_args = tool_call['arguments'] || tool_call[:arguments] || {}
@@ -626,480 +295,403 @@
626
295
  tool_duration = tool_call['duration_ms'] || tool_call[:duration_ms]
627
296
  tool_called_at = tool_call['called_at'] || tool_call[:called_at]
628
297
 
629
- # Status badge styling
630
- status_badge_class = case tool_status
631
- when 'success' then 'bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300'
632
- when 'error' then 'bg-red-100 dark:bg-red-900/50 text-red-700 dark:text-red-300'
633
- 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'
634
302
  end
635
303
  %>
636
- <div class="border border-gray-100 dark:border-gray-700 rounded-lg overflow-hidden">
637
- <!-- Tool Call Header -->
638
- <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">
639
306
  <div class="flex items-center justify-between">
640
- <div class="flex items-center gap-3">
641
- <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">
642
- <%= index + 1 %>
643
- </span>
644
- <code class="text-sm font-semibold text-gray-900 dark:text-gray-100"><%= tool_name %></code>
645
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= status_badge_class %>">
646
- <%= tool_status %>
647
- </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>
648
311
  <% if tool_duration.present? %>
649
- <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">
650
- <%= tool_duration %>ms
651
- </span>
312
+ <span class="badge badge-sm badge-purple"><%= tool_duration %>ms</span>
652
313
  <% end %>
653
314
  </div>
654
- <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">
655
316
  <% if tool_called_at.present? %>
656
- <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>
657
318
  <% end %>
658
319
  <% if tool_id.present? %>
659
- <span class="font-mono truncate max-w-[120px]" title="<%= tool_id %>">
660
- <%= tool_id.to_s.truncate(16) %>
661
- </span>
320
+ <span class="truncate max-w-[120px]" title="<%= tool_id %>"><%= tool_id.to_s.truncate(16) %></span>
662
321
  <% end %>
663
322
  </div>
664
323
  </div>
665
324
  </div>
666
325
 
667
- <!-- Tool Call Arguments -->
668
326
  <% if tool_args.present? && tool_args.any? %>
669
- <div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700">
670
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Arguments</p>
671
- <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>
672
330
  </div>
673
331
  <% else %>
674
- <div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700">
675
- <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>
676
334
  </div>
677
335
  <% end %>
678
336
 
679
- <!-- Tool Call Result (NEW) -->
680
337
  <% if tool_result.present? %>
681
- <div class="px-4 py-3 border-t border-gray-100 dark:border-gray-700">
682
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-2">Result</p>
683
- <div class="bg-gray-50 dark:bg-gray-900 rounded-lg p-3 max-h-48 overflow-y-auto">
684
- <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>
685
342
  </div>
686
343
  </div>
687
344
  <% end %>
688
345
 
689
- <!-- Tool Call Error (NEW) -->
690
346
  <% if tool_status == 'error' && tool_error.present? %>
691
- <div class="px-4 py-3 border-t border-red-100 dark:border-red-900/50 bg-red-50 dark:bg-red-900/20">
692
- <p class="text-xs text-red-600 dark:text-red-400 uppercase tracking-wide mb-2">Error</p>
693
- <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>
694
350
  </div>
695
351
  <% end %>
696
352
  </div>
697
353
  <% end %>
698
354
  </div>
699
355
  <% else %>
700
- <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>
701
357
  <% end %>
702
358
  </div>
703
359
 
704
- <!-- Metadata -->
360
+ <!-- ── metadata ──────────────────────── -->
705
361
  <% if @execution.metadata.present? && @execution.metadata.any? %>
706
- <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 }">
707
- <div class="flex items-center justify-between">
708
- <div class="flex items-center gap-2">
709
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Metadata</h3>
710
- <span class="text-xs text-gray-400 dark:text-gray-500">(<%= @execution.metadata.keys.count %> keys)</span>
711
- </div>
712
- <div class="flex items-center gap-2">
713
- <button
714
- type="button"
715
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
716
- 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"
717
- >
718
- <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
719
- <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"/>
720
- </svg>
721
- <svg class="w-4 h-4 check-icon hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
722
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
723
- </svg>
724
- <span>Copy</span>
725
- </button>
726
- <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">
727
- <span x-text="expanded ? 'Collapse' : 'Expand'">Expand</span>
728
- </button>
729
- </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>
730
374
  </div>
731
- <div x-show="expanded" x-cloak class="mt-4">
732
- <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>
733
377
  </div>
734
378
  </div>
735
379
  <% end %>
736
380
 
737
- <!-- Execution Hierarchy -->
381
+ <!-- ── hierarchy ──────────────────────── -->
738
382
  <% if @execution.parent_execution_id.present? || (@execution.respond_to?(:child_executions) && @execution.child_executions.any?) %>
739
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
740
- <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>
741
387
 
388
+ <div class="font-mono text-xs space-y-1.5">
742
389
  <% if @execution.parent_execution_id.present? %>
743
- <div class="mb-4">
744
- <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:
745
392
  <%= link_to "##{@execution.parent_execution_id}",
746
393
  ruby_llm_agents.execution_path(@execution.parent_execution_id),
747
- 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" %>
748
395
  </div>
749
396
  <% end %>
750
397
 
751
398
  <% if @execution.respond_to?(:child_executions) && @execution.child_executions.any? %>
752
- <div>
753
- <span class="text-xs text-gray-500 dark:text-gray-400">Child Executions (<%= @execution.child_executions.count %>):</span>
754
- <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">
755
402
  <% @execution.child_executions.limit(10).each do |child| %>
756
403
  <%= link_to "##{child.id}",
757
404
  ruby_llm_agents.execution_path(child),
758
- 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" %>
759
406
  <% end %>
760
407
  <% if @execution.child_executions.count > 10 %>
761
- <span class="text-xs text-gray-400">+<%= @execution.child_executions.count - 10 %> more</span>
408
+ <span>+<%= @execution.child_executions.count - 10 %> more</span>
762
409
  <% end %>
763
- </div>
410
+ </span>
764
411
  </div>
765
412
  <% end %>
766
413
  </div>
767
414
  <% end %>
768
415
 
769
- <!-- System Prompt -->
416
+ <!-- ── system prompt ──────────────────── -->
770
417
  <% if @execution.system_prompt.present? %>
771
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
772
- <div class="flex items-center justify-between mb-4">
773
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">System Prompt</h3>
774
- <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">
775
- <span id="system-prompt-toggle">Expand</span>
776
- </button>
777
- </div>
778
- <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>
779
- <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>
780
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>
781
427
  <% end %>
782
428
 
783
- <!-- User Prompt -->
429
+ <!-- ── user prompt ──────────────────── -->
784
430
  <% if @execution.user_prompt.present? %>
785
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
786
- <div class="flex items-center justify-between mb-4">
787
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">User Prompt</h3>
788
- <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">
789
- <span id="user-prompt-toggle">Expand</span>
790
- </button>
791
- </div>
792
- <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>
793
- <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>
794
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>
795
440
  <% end %>
796
441
 
797
- <!-- Conversation Context -->
442
+ <!-- ── conversation ──────────────────── -->
798
443
  <% if @execution.respond_to?(:messages_count) && @execution.messages_count.to_i > 0 %>
799
444
  <%
800
445
  messages_summary = @execution.messages_summary || {}
801
- # Handle both string and symbol keys
802
446
  first_message = messages_summary["first"] || messages_summary[:first]
803
447
  last_message = messages_summary["last"] || messages_summary[:last]
804
448
  max_len = RubyLLM::Agents.configuration.messages_summary_max_length || 500
805
449
  %>
806
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
807
- <div class="flex items-center gap-2 mb-4">
808
- <svg class="w-5 h-5 text-indigo-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
809
- <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"/>
810
- </svg>
811
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Conversation Context</h3>
812
- <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">
813
- <%= @execution.messages_count %> message<%= @execution.messages_count == 1 ? '' : 's' %>
814
- </span>
815
- </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>
816
454
 
817
- <div class="space-y-4">
818
- <% if first_message %>
819
- <%
820
- first_role = first_message["role"] || first_message[:role] || "unknown"
821
- first_content = first_message["content"] || first_message[:content] || ""
822
- first_truncated = first_content.length >= max_len
823
- role_badge_class = case first_role.to_s
824
- when "user" then "bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300"
825
- when "assistant" then "bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300"
826
- when "system" then "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300"
827
- else "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300"
828
- end
829
- %>
830
- <div class="border border-gray-100 dark:border-gray-700 rounded-lg p-4">
831
- <div class="flex items-center gap-2 mb-2">
832
- <span class="text-xs text-gray-500 dark:text-gray-400 font-medium">First Message</span>
833
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= role_badge_class %>">
834
- <%= first_role %>
835
- </span>
836
- <% if first_truncated %>
837
- <span class="text-xs text-gray-400 dark:text-gray-500 italic">(truncated)</span>
838
- <% end %>
839
- </div>
840
- <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 %>
841
475
  </div>
842
- <% 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 %>
843
479
 
844
- <% if last_message %>
845
- <%
846
- last_role = last_message["role"] || last_message[:role] || "unknown"
847
- last_content = last_message["content"] || last_message[:content] || ""
848
- last_truncated = last_content.length >= max_len
849
- role_badge_class = case last_role.to_s
850
- when "user" then "bg-blue-100 dark:bg-blue-900/50 text-blue-800 dark:text-blue-300"
851
- when "assistant" then "bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300"
852
- when "system" then "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300"
853
- else "bg-gray-100 dark:bg-gray-700 text-gray-800 dark:text-gray-300"
854
- end
855
- %>
856
- <div class="border border-gray-100 dark:border-gray-700 rounded-lg p-4">
857
- <div class="flex items-center gap-2 mb-2">
858
- <span class="text-xs text-gray-500 dark:text-gray-400 font-medium">Last Message</span>
859
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <%= role_badge_class %>">
860
- <%= last_role %>
861
- </span>
862
- <% if last_truncated %>
863
- <span class="text-xs text-gray-400 dark:text-gray-500 italic">(truncated)</span>
864
- <% end %>
865
- </div>
866
- <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 %>
867
499
  </div>
868
- <% 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 %>
869
503
 
870
- <% if @execution.messages_count > 2 %>
871
- <p class="text-xs text-gray-400 dark:text-gray-500 text-center">
872
- + <%= @execution.messages_count - 2 %> more message<%= @execution.messages_count - 2 == 1 ? '' : 's' %> in between
873
- </p>
874
- <% end %>
875
- </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 %>
876
509
  </div>
877
510
  <% end %>
878
511
 
879
- <!-- Diagnostics Panel -->
880
- <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
881
- <div class="flex items-center justify-between mb-4">
882
- <div class="flex items-center gap-2">
883
- <svg class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
884
- <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"/>
885
- </svg>
886
- <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Diagnostics</h3>
887
- </div>
888
- <button
889
- type="button"
890
- id="toggle-diagnostics-btn"
891
- onclick="toggleDiagnostics()"
892
- 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"
893
- >
894
- <svg id="diagnostics-expand-icon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
895
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
896
- </svg>
897
- <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>
898
521
  </button>
899
522
  </div>
900
523
 
901
- <!-- Quick Info (always visible) -->
902
- <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
903
- <div>
904
- <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Model</span>
905
- <p class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.model_id %></p>
906
- </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 -->
907
536
  <div>
908
- <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Temperature</span>
909
- <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>
910
562
  </div>
563
+
564
+ <!-- Performance -->
911
565
  <div>
912
- <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Version</span>
913
- <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>
914
585
  </div>
586
+
587
+ <!-- Cost -->
915
588
  <div>
916
- <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Status</span>
917
- <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>
918
604
  </div>
919
- </div>
920
605
 
921
- <!-- Expanded Details (hidden by default) -->
922
- <div id="diagnostics-details" class="hidden mt-6 pt-6 border-t border-gray-100 dark:border-gray-700">
923
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
924
- <!-- Timing Information -->
925
- <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
926
- <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Timing</h4>
927
- <dl class="space-y-2 text-sm">
928
- <div class="flex justify-between">
929
- <dt class="text-gray-500 dark:text-gray-400">Started</dt>
930
- <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>
931
- </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 %>
932
619
  <div class="flex justify-between">
933
- <dt class="text-gray-500 dark:text-gray-400">Completed</dt>
934
- <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>
935
622
  </div>
623
+ <% end %>
624
+ <% if @execution.respond_to?(:attempts_count) && @execution.attempts_count && @execution.attempts_count > 1 %>
936
625
  <div class="flex justify-between">
937
- <dt class="text-gray-500 dark:text-gray-400">Duration</dt>
938
- <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>
939
628
  </div>
940
- <% 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? %>
941
639
  <div class="flex justify-between">
942
- <dt class="text-gray-500 dark:text-gray-400">Time to First Token</dt>
943
- <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>
944
654
  </div>
945
655
  <% end %>
946
- <div class="flex justify-between">
947
- <dt class="text-gray-500 dark:text-gray-400">Created At</dt>
948
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
949
- </div>
950
- </dl>
951
- </div>
952
-
953
- <!-- Performance Metrics -->
954
- <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
955
- <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Performance</h4>
956
- <dl class="space-y-2 text-sm">
957
- <div class="flex justify-between">
958
- <dt class="text-gray-500 dark:text-gray-400">Tokens/Second</dt>
959
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.tokens_per_second&.round(1) || 'N/A' %></dd>
960
- </div>
961
- <div class="flex justify-between">
962
- <dt class="text-gray-500 dark:text-gray-400">Input Tokens</dt>
963
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.input_tokens || 0 %></dd>
964
- </div>
965
- <div class="flex justify-between">
966
- <dt class="text-gray-500 dark:text-gray-400">Output Tokens</dt>
967
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.output_tokens || 0 %></dd>
968
- </div>
969
- <div class="flex justify-between">
970
- <dt class="text-gray-500 dark:text-gray-400">Cached Tokens</dt>
971
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.cached_tokens || 0 %></dd>
972
- </div>
973
- </dl>
974
- </div>
975
-
976
- <!-- Cost Breakdown -->
977
- <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
978
- <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Cost</h4>
979
- <dl class="space-y-2 text-sm">
980
- <div class="flex justify-between">
981
- <dt class="text-gray-500 dark:text-gray-400">Input Cost</dt>
982
- <dd class="font-mono text-gray-900 dark:text-gray-100">$<%= number_with_precision(@execution.input_cost || 0, precision: 6) %></dd>
983
- </div>
984
- <div class="flex justify-between">
985
- <dt class="text-gray-500 dark:text-gray-400">Output Cost</dt>
986
- <dd class="font-mono text-gray-900 dark:text-gray-100">$<%= number_with_precision(@execution.output_cost || 0, precision: 6) %></dd>
987
- </div>
988
- <div class="flex justify-between font-semibold">
989
- <dt class="text-gray-600 dark:text-gray-300">Total Cost</dt>
990
- <dd class="font-mono text-gray-900 dark:text-gray-100">$<%= number_with_precision(@execution.total_cost || 0, precision: 6) %></dd>
991
- </div>
992
656
  </dl>
993
657
  </div>
658
+ <% end %>
994
659
 
995
- <!-- Configuration -->
996
- <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
997
- <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Configuration</h4>
998
- <dl class="space-y-2 text-sm">
999
- <div class="flex justify-between">
1000
- <dt class="text-gray-500 dark:text-gray-400">Agent Type</dt>
1001
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.agent_type %></dd>
1002
- </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">
1003
665
  <div class="flex justify-between">
1004
- <dt class="text-gray-500 dark:text-gray-400">Temperature</dt>
1005
- <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>
1006
668
  </div>
1007
- <% 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? %>
1008
670
  <div class="flex justify-between">
1009
- <dt class="text-gray-500 dark:text-gray-400">Chosen Model</dt>
1010
- <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>
1011
673
  </div>
1012
674
  <% end %>
1013
- <% if @execution.respond_to?(:attempts_count) && @execution.attempts_count && @execution.attempts_count > 1 %>
675
+ <% if @execution.cached_at.present? %>
1014
676
  <div class="flex justify-between">
1015
- <dt class="text-gray-500 dark:text-gray-400">Retry Count</dt>
1016
- <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>
1017
679
  </div>
1018
680
  <% end %>
1019
681
  </dl>
1020
682
  </div>
1021
- </div>
1022
-
1023
- <!-- Second Row: Tracing and Caching -->
1024
- <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
1025
- <!-- Tracing Information -->
1026
- <% if @execution.trace_id.present? || @execution.request_id.present? %>
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">Tracing</h4>
1029
- <dl class="space-y-2 text-sm">
1030
- <% if @execution.request_id.present? %>
1031
- <div class="flex justify-between">
1032
- <dt class="text-gray-500 dark:text-gray-400">Request ID</dt>
1033
- <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs"><%= @execution.request_id %></dd>
1034
- </div>
1035
- <% end %>
1036
- <% if @execution.trace_id.present? %>
1037
- <div class="flex justify-between">
1038
- <dt class="text-gray-500 dark:text-gray-400">Trace ID</dt>
1039
- <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs"><%= @execution.trace_id %></dd>
1040
- </div>
1041
- <% end %>
1042
- <% if @execution.span_id.present? %>
1043
- <div class="flex justify-between">
1044
- <dt class="text-gray-500 dark:text-gray-400">Span ID</dt>
1045
- <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs"><%= @execution.span_id %></dd>
1046
- </div>
1047
- <% end %>
1048
- </dl>
1049
- </div>
1050
- <% end %>
1051
-
1052
- <!-- Caching Information -->
1053
- <% if @execution.cache_hit || @execution.response_cache_key.present? %>
1054
- <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
1055
- <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Caching</h4>
1056
- <dl class="space-y-2 text-sm">
1057
- <div class="flex justify-between">
1058
- <dt class="text-gray-500 dark:text-gray-400">Cache Hit</dt>
1059
- <dd class="font-mono text-gray-900 dark:text-gray-100">
1060
- <% if @execution.cache_hit %>
1061
- <span class="text-green-600 dark:text-green-400">Yes</span>
1062
- <% else %>
1063
- <span class="text-gray-400">No</span>
1064
- <% end %>
1065
- </dd>
1066
- </div>
1067
- <% if @execution.response_cache_key.present? %>
1068
- <div class="flex justify-between">
1069
- <dt class="text-gray-500 dark:text-gray-400">Cache Key</dt>
1070
- <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs truncate max-w-xs" title="<%= @execution.response_cache_key %>">
1071
- <%= @execution.response_cache_key.truncate(30) %>
1072
- </dd>
1073
- </div>
1074
- <% end %>
1075
- <% if @execution.cached_at.present? %>
1076
- <div class="flex justify-between">
1077
- <dt class="text-gray-500 dark:text-gray-400">Cached At</dt>
1078
- <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.cached_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
1079
- </div>
1080
- <% end %>
1081
- </dl>
1082
- </div>
1083
- <% end %>
1084
- </div>
683
+ <% end %>
1085
684
 
1086
- <!-- Execution ID -->
1087
- <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
1088
- <div class="flex items-center justify-between">
1089
- <span class="text-xs text-gray-500 dark:text-gray-400">
1090
- Execution ID: <code class="bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded font-mono"><%= @execution.id %></code>
1091
- </span>
1092
- <button
1093
- type="button"
1094
- onclick="copyDiagnostics()"
1095
- 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"
1096
- >
1097
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
1098
- <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"/>
1099
- </svg>
1100
- Copy All
1101
- </button>
1102
- </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>
1103
695
  </div>
1104
696
  </div>
1105
697
  </div>
@@ -1113,88 +705,36 @@
1113
705
  const isHidden = content.classList.contains('hidden');
1114
706
  content.classList.toggle('hidden', !isHidden);
1115
707
  if (preview) preview.classList.toggle('hidden', isHidden);
1116
- toggle.textContent = isHidden ? 'Collapse' : 'Expand';
708
+ toggle.textContent = isHidden ? 'collapse' : 'expand';
1117
709
  }
1118
710
 
1119
711
  document.addEventListener('DOMContentLoaded', function() {
1120
712
  document.querySelectorAll('.copy-json-btn').forEach(function(button) {
1121
713
  button.addEventListener('click', function() {
1122
714
  const jsonText = atob(this.getAttribute('data-copy-json'));
1123
- const span = this.querySelector('span');
1124
- const copyIcon = this.querySelector('.copy-icon');
1125
- const checkIcon = this.querySelector('.check-icon');
715
+ const btn = this;
716
+ const originalText = btn.textContent;
1126
717
 
1127
718
  navigator.clipboard.writeText(jsonText).then(function() {
1128
- span.textContent = 'Copied!';
1129
- copyIcon.classList.add('hidden');
1130
- checkIcon.classList.remove('hidden');
1131
- button.classList.add('text-green-600');
1132
-
719
+ btn.textContent = 'copied!';
1133
720
  setTimeout(function() {
1134
- span.textContent = 'Copy';
1135
- copyIcon.classList.remove('hidden');
1136
- checkIcon.classList.add('hidden');
1137
- button.classList.remove('text-green-600');
721
+ btn.textContent = originalText;
1138
722
  }, 2000);
1139
723
  }).catch(function(err) {
1140
724
  console.error('Failed to copy:', err);
1141
- span.textContent = 'Failed';
725
+ btn.textContent = 'failed';
1142
726
  setTimeout(function() {
1143
- span.textContent = 'Copy';
727
+ btn.textContent = originalText;
1144
728
  }, 2000);
1145
729
  });
1146
730
  });
1147
731
  });
1148
732
  });
1149
733
 
1150
- // Rerun modal functions
1151
- function confirmRerun() {
1152
- document.getElementById('rerun-modal').classList.remove('hidden');
1153
- document.body.classList.add('overflow-hidden');
1154
- }
1155
-
1156
- function closeRerunModal() {
1157
- document.getElementById('rerun-modal').classList.add('hidden');
1158
- document.body.classList.remove('overflow-hidden');
1159
- }
1160
-
1161
- // Close modal on Escape key
1162
- document.addEventListener('keydown', function(e) {
1163
- if (e.key === 'Escape') {
1164
- closeRerunModal();
1165
- }
1166
- });
1167
-
1168
- // Diagnostics panel toggle (default to expanded)
1169
- let diagnosticsExpanded = localStorage.getItem('ruby_llm_agents_diagnostics_expanded') !== 'false';
1170
-
1171
- function toggleDiagnostics() {
1172
- diagnosticsExpanded = !diagnosticsExpanded;
1173
- localStorage.setItem('ruby_llm_agents_diagnostics_expanded', diagnosticsExpanded);
1174
- updateDiagnosticsUI();
1175
- }
1176
-
1177
- function updateDiagnosticsUI() {
1178
- const details = document.getElementById('diagnostics-details');
1179
- const toggleText = document.getElementById('diagnostics-toggle-text');
1180
- const expandIcon = document.getElementById('diagnostics-expand-icon');
1181
-
1182
- if (diagnosticsExpanded) {
1183
- details.classList.remove('hidden');
1184
- toggleText.textContent = 'Collapse';
1185
- expandIcon.style.transform = 'rotate(180deg)';
1186
- } else {
1187
- details.classList.add('hidden');
1188
- toggleText.textContent = 'Expand';
1189
- expandIcon.style.transform = 'rotate(0deg)';
1190
- }
1191
- }
1192
-
1193
734
  function copyDiagnostics() {
1194
735
  const diagnostics = {
1195
736
  execution_id: <%= @execution.id %>,
1196
737
  agent_type: "<%= @execution.agent_type %>",
1197
- agent_version: "<%= @execution.agent_version || '1.0' %>",
1198
738
  model_id: "<%= @execution.model_id %>",
1199
739
  status: "<%= @execution.status %>",
1200
740
  temperature: <%= @execution.temperature || 'null' %>,
@@ -1211,16 +751,15 @@
1211
751
  created_at: "<%= @execution.created_at.iso8601 %>"
1212
752
  };
1213
753
 
754
+ const btn = event.currentTarget;
1214
755
  navigator.clipboard.writeText(JSON.stringify(diagnostics, null, 2)).then(function() {
1215
- alert('Diagnostics copied to clipboard!');
756
+ btn.textContent = 'copied!';
757
+ setTimeout(function() {
758
+ btn.textContent = 'copy all';
759
+ }, 2000);
1216
760
  }).catch(function(err) {
1217
761
  console.error('Failed to copy diagnostics:', err);
1218
762
  });
1219
763
  }
1220
-
1221
- // Initialize diagnostics panel on page load
1222
- document.addEventListener('DOMContentLoaded', function() {
1223
- updateDiagnosticsUI();
1224
- });
1225
764
  </script>
1226
765
  </div>