Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Fixed

- **Keep the trace console usable offline** (`actionagent`). The console
ships its own inline styles and uses native details and ordinary links
instead of CDN-hosted Tailwind, Turbo and Stimulus. Automatic refresh is
opt-in (30 or 60 seconds) and uses the browser's refresh mechanism;
expanding a trace no longer depends on JavaScript or a host asset pipeline.

### Changed

- **One badge for every status and message role in the dashboard**
Expand Down
6 changes: 6 additions & 0 deletions actionagent/app/controllers/action_agent/traces_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ module ActionAgent
# - Metrics overview
# - Live updates via Turbo Streams
class TracesController < ApplicationController
before_action :set_refresh_interval

def index
@traces = fetch_traces
@metrics = calculate_metrics
Expand Down Expand Up @@ -38,6 +40,10 @@ def metrics

private

def set_refresh_interval
@refresh_interval = params[:refresh].to_i if %w[30 60].include?(params[:refresh])
end

def turbo_stream_available?
Mime::Type.lookup_by_extension(:turbo_stream).present?
end
Expand Down
45 changes: 45 additions & 0 deletions actionagent/app/views/action_agent/traces/_styles.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<style>
.trace-console { color-scheme: light dark; --background: #f8fafc; --surface: #fff; --text: #172033; --muted: #475569; --border: #cbd5e1; --link: #3730a3; --danger: #b91c1c; --success: #166534; margin: 0; background: var(--background); color: var(--text); font: 16px/1.5 system-ui, sans-serif; }
.trace-console * { box-sizing: border-box; }
.trace-console header { background: var(--surface); border-bottom: 1px solid var(--border); }
.trace-console nav, .trace-console main { max-width: 1200px; margin: auto; padding: 1rem; }
.trace-console nav, .trace-console .refresh-controls, .trace-console .tokens { display: flex; flex-wrap: wrap; gap: 1rem; align-items: center; }
.trace-console a { color: var(--link); text-underline-offset: .2em; }
.trace-console a:focus-visible, .trace-console summary:focus-visible { outline: 3px solid var(--link); outline-offset: 3px; }
.trace-console .muted, .trace-console dt { color: var(--muted); }
.trace-console .refresh-controls { justify-content: flex-end; margin-bottom: 1rem; }
.trace-console h1 { font-size: 1.5rem; }
.trace-console h2 { font-size: 1.2rem; }
.trace-console .panel { background: var(--surface); border: 1px solid var(--border); border-radius: .5rem; padding: 1rem; margin: 1rem 0; }
.trace-console .summary, .trace-console .metadata { display: grid; grid-template-columns: repeat(auto-fit, minmax(160px, 1fr)); gap: 1rem; margin: 1rem 0; }
.trace-console .summary > div { padding: 1rem; background: var(--surface); border: 1px solid var(--border); border-radius: .5rem; }
.trace-console dt { font-size: .875rem; }
.trace-console dd { margin: 0; overflow-wrap: anywhere; }
.trace-console .summary dd { font-size: 1.4rem; font-weight: 600; }
.trace-console details { margin: .5rem 0; }
.trace-console summary { cursor: pointer; padding: .5rem 0; }
.trace-console .trace-summary { display: inline-flex; flex-wrap: wrap; gap: .5rem 1rem; align-items: baseline; }
.trace-console .badge { border: 1px solid currentColor; border-radius: .25rem; padding: .1rem .4rem; font-size: .8rem; }
.trace-console .error { color: var(--danger); }
.trace-console .success { color: var(--success); }
.trace-console .error-message { border-left: 4px solid var(--danger); padding: .5rem 1rem; overflow-wrap: anywhere; }
.trace-console pre { font: .8rem/1.5 ui-monospace, monospace; overflow: auto; padding: 1rem; background: var(--background); border: 1px solid var(--border); }
.trace-console .table-scroll { overflow-x: auto; }
.trace-console table { width: 100%; border-collapse: collapse; text-align: left; }
.trace-console th, .trace-console td { padding: .75rem; border-bottom: 1px solid var(--border); }
.trace-console .span-row { display: grid; grid-template-columns: minmax(5rem, 9rem) 1fr 5rem; gap: .5rem; align-items: center; margin: .5rem 0; font-size: .8rem; }
.trace-console .span-name { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; }
.trace-console .span-track { position: relative; height: 1.25rem; background: var(--background); border: 1px solid var(--border); overflow: hidden; border-radius: .2rem; }
.trace-console .span-bar { position: absolute; height: 100%; background: #6366f1; }
.trace-console .span-bar[data-type="tool"] { background: #b45309; }
.trace-console .span-bar[data-type="thinking"] { background: #9333ea; }
.trace-console .span-bar[data-type="response"] { background: #15803d; }
.trace-console .span-bar[data-status="ERROR"] { background: var(--danger); }
@media (prefers-color-scheme: dark) {
.trace-console { --background: #111827; --surface: #1f2937; --text: #f1f5f9; --muted: #cbd5e1; --border: #64748b; --link: #c7d2fe; --danger: #fca5a5; --success: #86efac; }
}
@media (max-width: 480px) {
.trace-console main { padding: .5rem; }
.trace-console .span-row { grid-template-columns: 5rem 1fr 4rem; }
}
</style>
8 changes: 8 additions & 0 deletions actionagent/app/views/action_agent/traces/_summary.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<dl class="summary" aria-label="Last 24 hours">
<div><dt>Total Traces</dt><dd><%= number_with_delimiter(@metrics[:total_traces]) %></dd></div>
<div><dt>Total Tokens</dt><dd><%= number_to_human(@metrics[:total_tokens], precision: 2) %></dd></div>
<div><dt>Avg Duration</dt><dd><%= @metrics[:avg_duration_ms] >= 1000 ? "#{(@metrics[:avg_duration_ms] / 1000.0).round(2)}s" : "#{@metrics[:avg_duration_ms].round(0)}ms" %></dd></div>
<div><dt>Error Rate</dt><dd class="<%= 'error' if @metrics[:error_rate] > 5 %>"><%= @metrics[:error_rate] %>%</dd></div>
<div><dt>Active Agents</dt><dd><%= @metrics[:unique_agents] %></dd></div>
</dl>
<p class="muted">Last 24h</p>
129 changes: 33 additions & 96 deletions actionagent/app/views/action_agent/traces/_trace_detail.html.erb
Original file line number Diff line number Diff line change
@@ -1,64 +1,22 @@
<div class="space-y-4">
<!-- Trace Info -->
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
<div>
<span class="text-gray-500 dark:text-gray-400">Trace ID</span>
<div class="font-mono text-gray-900 dark:text-white"><%= trace.trace_id %></div>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Service</span>
<div class="text-gray-900 dark:text-white"><%= trace.service_name || "—" %></div>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Provider</span>
<div class="text-gray-900 dark:text-white"><%= trace.provider || "—" %></div>
</div>
<div>
<span class="text-gray-500 dark:text-gray-400">Model</span>
<div class="text-gray-900 dark:text-white"><%= trace.model || "—" %></div>
</div>
</div>

<% if trace.error? && trace.error_message.present? %>
<div class="bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded p-3">
<div class="flex">
<svg class="h-5 w-5 text-red-400" viewBox="0 0 20 20" fill="currentColor">
<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" />
</svg>
<div class="ml-3">
<h3 class="text-sm font-medium text-red-800 dark:text-red-200">Error</h3>
<p class="mt-1 text-sm text-red-700 dark:text-red-300"><%= trace.error_message %></p>
</div>
</div>
</div>
<dl class="metadata">
<div><dt>Trace ID</dt><dd><%= trace.trace_id %></dd></div>
<div><dt>Service</dt><dd><%= trace.service_name || "Unknown" %></dd></div>
<div><dt>Provider</dt><dd><%= trace.provider || "Unknown" %></dd></div>
<div><dt>Model</dt><dd><%= trace.model || "Unknown" %></dd></div>
</dl>
<% if trace.error? && trace.error_message.present? %>
<p class="error-message error"><strong>Error:</strong> <%= trace.error_message %></p>
<% end %>
<div class="tokens">
<span>Input: <%= number_with_delimiter(trace.total_input_tokens || 0) %></span>
<span>Output: <%= number_with_delimiter(trace.total_output_tokens || 0) %></span>
<% if trace.total_thinking_tokens.to_i > 0 %>
<span>Thinking: <%= number_with_delimiter(trace.total_thinking_tokens) %></span>
<% end %>

<!-- Token Breakdown -->
<div class="flex gap-6 text-sm">
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-blue-500"></div>
<span class="text-gray-500">Input:</span>
<span class="font-medium text-gray-900 dark:text-white"><%= number_with_delimiter(trace.total_input_tokens || 0) %></span>
</div>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-green-500"></div>
<span class="text-gray-500">Output:</span>
<span class="font-medium text-gray-900 dark:text-white"><%= number_with_delimiter(trace.total_output_tokens || 0) %></span>
</div>
<% if trace.total_thinking_tokens.to_i > 0 %>
<div class="flex items-center gap-2">
<div class="w-3 h-3 rounded-full bg-purple-500"></div>
<span class="text-gray-500">Thinking:</span>
<span class="font-medium text-purple-600 dark:text-purple-400"><%= number_with_delimiter(trace.total_thinking_tokens) %></span>
</div>
<% end %>
</div>

<!-- Spans Timeline -->
<% if trace.spans.present? %>
<div class="mt-4">
<h4 class="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">Span Timeline</h4>
<div class="relative bg-gray-100 dark:bg-gray-800 rounded-lg p-4">
</div>
<% if trace.spans.present? %>
<section aria-label="Span timeline">
<h2>Span Timeline</h2>
<% total_duration = (trace.total_duration_ms || 1).to_f %>
<% total_duration = 1.0 if total_duration.zero? %>
<%
Expand All @@ -77,41 +35,20 @@
start_pct = ((relative_ms || 0).to_f / total_duration * 100).clamp(0, 100)
width_pct = ((span["duration_ms"] || 0).to_f / total_duration * 100).clamp(0.5, [ 100 - start_pct, 0.5 ].max)
span_type = span["type"] || "unknown"
colors = {
"root" => "bg-gray-400",
"prompt" => "bg-blue-400",
"llm" => "bg-indigo-500",
"generate" => "bg-indigo-400",
"tool" => "bg-amber-500",
"thinking" => "bg-purple-500",
"response" => "bg-green-400"
}
bg_color = colors[span_type] || "bg-gray-400"
%>
<div class="flex items-center gap-2 mb-2">
<div class="w-24 text-xs text-gray-500 dark:text-gray-400 truncate" title="<%= span["name"] %>">
<%= span["name"]&.split(".")&.last || span_type %>
</div>
<div class="flex-1 h-6 bg-gray-200 dark:bg-gray-700 rounded relative overflow-hidden">
<div class="span-bar absolute h-full <%= bg_color %> rounded <%= span["status"] == "ERROR" ? 'opacity-50 bg-red-500' : '' %>"
style="left: <%= start_pct %>%; width: <%= width_pct %>%;"
title="<%= span["name"] %>: <%= span["duration_ms"]&.round(1) %>ms">
</div>
</div>
<div class="w-16 text-xs text-gray-500 dark:text-gray-400 text-right">
<%= span["duration_ms"] ? "#{span["duration_ms"].round(1)}ms" : "—" %>
</div>
</div>
<% end %>
<div class="span-row">
<span class="span-name" title="<%= span["name"] %>"><%= span["name"]&.split(".")&.last || span_type %></span>
<div class="span-track">
<div class="span-bar" data-type="<%= span_type %>" data-status="<%= span["status"] %>"
style="left: <%= start_pct %>%; width: <%= width_pct %>%;"
title="<%= span["name"] %>: <%= span["duration_ms"]&.round(1) %>ms"></div>
</div>
<span><%= span["duration_ms"] ? "#{span["duration_ms"].round(1)}ms" : "Unknown" %></span>
</div>
</div>
<% end %>

<!-- Raw Data (collapsed) -->
<details class="mt-4">
<summary class="text-sm text-gray-500 dark:text-gray-400 cursor-pointer hover:text-gray-700">
View raw trace data
</summary>
<pre class="mt-2 p-3 bg-gray-100 dark:bg-gray-800 rounded text-xs overflow-x-auto"><%= JSON.pretty_generate(trace.as_json(except: [:id, :created_at, :updated_at])) %></pre>
</details>
</div>
<% end %>
</section>
<% end %>
<details>
<summary>View raw trace data</summary>
<pre><%= JSON.pretty_generate(trace.as_json(except: [ :id, :created_at, :updated_at ])) %></pre>
</details>
Loading
Loading