transcript-viewer
Pure Ruby renderer for LLM/agent transcript event hashes. It generates a self-contained HTML page with embedded CSS and JavaScript.
Usage
require "transcript_viewer"
html = TranscriptViewer.render(
session_id: 103,
events: [
{
id: "1",
parent_id: nil,
timestamp: Time.now.iso8601,
type: "message",
usage: nil,
data: {
role: "assistant",
content: [{ type: "text", text: "Hello" }],
},
},
],
export_href: "/admin/agent_session/103/export",
)
File.write("session.html", html)
Composable rendering
Use TranscriptViewer.page when the host application owns the surrounding page chrome. The page normalizes the transcript once and can render its stylesheet, interactive transcript application, and JavaScript separately:
page = TranscriptViewer.page(session_id: 103, events: events)
page.stylesheet # CSS string
page.app_html # Sidebar and transcript content
page.javascript # Script tag for transcript interactions
page.render # The complete standalone HTML document
Pass trusted host markup with before_html: and after_html: to place application controls above and below the transcript within app_html:
page = TranscriptViewer.page(
session_id: 103,
events: events,
before_html: "<section>Run controls</section>",
after_html: "<form>Continue</form>",
)
The host is responsible for placing the stylesheet in its document head and the JavaScript after app_html.
Custom tool templates
Consumers can register an ERB for an exact tool name. Registration is normally done once during application startup:
TranscriptViewer.configure do |config|
config.register_tool_template(
"fetch_pull_requests",
File.("templates/tools/fetch_pull_requests.html.erb", __dir__),
)
end
A tool template receives a tool local and can use renderer helpers such as h and pretty_json:
<section class="pull-request-tool" data-status="<%= h(tool.status) %>">
<h2><%= h(tool.name) %></h2>
<pre><%= h(pretty_json(tool.input)) %></pre>
<% if tool.result_json %>
<pre><%= h(pretty_json(tool.result_json)) %></pre>
<% elsif tool.result_text != "" %>
<pre><%= h(tool.result_text) %></pre>
<% end %>
</section>
The tool execution interface includes:
name,id,input(also available asarguments)result,result_content,result_text,result_json, andimagesstatus,pending?,success?, anderror?callandresult_entryfor access to the normalized underlying hashes
Templates can also be supplied for one render. These override globally registered templates with the same tool name:
TranscriptViewer.render(
session_id: 103,
events: events,
tool_templates: {
"fetch_pull_requests" => "/path/to/fetch_pull_requests.html.erb",
},
)
Unregistered tools retain the built-in display. Server tools also use their existing built-in display for now.
Rails adapter example
This gem does not depend on Rails or ActiveSupport. In a Rails app, keep database access in the app and pass plain hashes into the renderer:
session = AgentSession.find(params[:session_id])
events = session.agent_session_events.order(:position).map do |event|
{
id: event.event_id,
parent_id: event.parent_id,
timestamp: event.&.iso8601,
type: event.event_type,
usage: event.usage_json,
data: event.data_json,
}
end
html = TranscriptViewer.render(
session_id: session.id,
events: events,
export_href: admin_export_agent_session_path(session.id),
forked_transcript: params[:forked_transcript],
ancestor_lookup: lambda do |event_id|
event = AgentSessionEvent.find_by(event_id: event_id)
next nil unless event
{
id: event.event_id,
parent_id: event.parent_id,
timestamp: event.&.iso8601,
type: event.event_type,
usage: event.usage_json,
data: event.data_json,
}
end,
)
render html: html.html_safe, layout: false
Supported event types
messagereasoning_changemodel_changecompaction
Message content supports text, thinking/reasoning, tool calls/results, and server tool use/results.