Current section
Files
Jump to
Current section
Files
lib/prompt_runner/rendering/renderers/compact.ex
defmodule PromptRunner.Rendering.Renderers.CompactRenderer do
@moduledoc """
Compact single-line token renderer for prompt-runner canonical events.
"""
@behaviour PromptRunner.Rendering.Renderer
@red "\e[0;31m"
@green "\e[0;32m"
@blue "\e[0;34m"
@magenta "\e[0;35m"
@dim "\e[2m"
@nc "\e[0m"
@tool_preview_limit 120
@result_preview_limit 2000
@type state :: map()
@impl true
def init(opts) do
{:ok,
%{
color: Keyword.get(opts, :color, true),
streaming: nil,
line_open: false,
in_text: false,
event_count: 0,
tool_count: 0,
current_tool: nil,
current_tool_id: nil
}}
end
@impl true
def render_event(%{hidden?: true}, state), do: {:ok, [], state}
def render_event(event, state) do
state = %{state | event_count: state.event_count + 1}
{iodata, state} = render(event, state)
{:ok, iodata, state}
end
@impl true
def finish(state) do
{closing, state} = end_stream(state)
summary = summary_line(state)
{:ok, [closing, summary], state}
end
defp render(%{type: :run_started, data: data}, state) do
{closing, state} = end_stream(state)
model = short_model(data[:model])
token = emit_token(state, [colorize("r+", @blue, state), model_label(model, state)])
{[closing, token], %{state | line_open: true}}
end
defp render(%{type: :message_streamed, data: data}, state) do
text = data[:delta] || data[:content] || ""
if state.streaming == :text do
{text, %{state | line_open: true, in_text: true}}
else
{closing, state} = end_stream(state)
prefix = space_if_open(state) <> colorize(">>", @magenta, state) <> " "
{[closing, prefix, text], %{state | streaming: :text, line_open: true, in_text: true}}
end
end
defp render(%{type: :tool_call_started, data: data}, state) do
{closing, state} = end_stream(state)
name = data[:tool_name] || "unknown"
token = emit_token(state, colorize("t+#{name}", @green, state))
state = %{
state
| tool_count: state.tool_count + 1,
current_tool: name,
current_tool_id: data[:tool_call_id],
line_open: true
}
{[closing, token], state}
end
defp render(%{type: :tool_call_completed, data: data}, state) do
{closing, state} = end_stream(state)
name = data[:tool_name] || "unknown"
token = emit_token(state, colorize("t-#{name}", @green, state))
result_token =
case data[:tool_output] do
nil ->
[]
"" ->
[]
output ->
[
" ",
colorize("tr:", @green, state),
dim(format_value(output, @result_preview_limit), state)
]
end
state = %{state | current_tool: nil, current_tool_id: nil}
{[closing, token | result_token], %{state | line_open: true}}
end
defp render(%{type: :tool_call_failed, data: data}, state) do
{closing, state} = end_stream(state)
name = data[:tool_name] || "unknown"
token =
emit_token(state, [
colorize("!", @red, state),
" ",
dim("tool #{name} failed", state)
])
{[closing, token], %{state | line_open: true, current_tool: nil, current_tool_id: nil}}
end
defp render(%{type: :token_usage_updated, data: data}, state) do
{closing, state} = end_stream(state)
input_t = data[:input_tokens] || 0
output_t = data[:output_tokens] || 0
cost_label = format_compact_cost(data[:cost_usd])
token = emit_token(state, dim("tk:#{input_t}/#{output_t}#{cost_label}", state))
{[closing, token], %{state | line_open: true}}
end
defp render(%{type: :message_received, data: data}, state) do
{closing, state} = end_stream(state)
content = data[:content] || ""
preview = truncate(content, @tool_preview_limit)
token = emit_token(state, [colorize("msg", @blue, state), " ", dim(preview, state)])
{[closing, token], %{state | in_text: false, line_open: true}}
end
defp render(%{type: :run_completed, data: data}, state) do
{closing, state} = end_stream(state)
reason = short_reason(data[:stop_reason])
cost_label = format_compact_cost(data[:cost_usd])
label = if reason == "", do: "r-", else: "r-:#{reason}"
token = emit_token(state, colorize(label <> cost_label, @blue, state))
{[closing, token], %{state | in_text: false, line_open: true}}
end
defp render(%{type: :run_failed, data: data}, state) do
{closing, state} = end_stream(state)
msg = data[:error_message] || "unknown error"
token =
emit_token(state, [
colorize("!", @red, state),
" ",
dim(truncate(msg, @tool_preview_limit), state)
])
{[closing, token], %{state | line_open: true}}
end
defp render(%{type: :run_cancelled}, state) do
{closing, state} = end_stream(state)
token = emit_token(state, [colorize("!", @red, state), " ", dim("cancelled", state)])
{[closing, token], %{state | line_open: true}}
end
defp render(%{type: :error_occurred, data: data}, state) do
{closing, state} = end_stream(state)
msg = data[:error_message] || "unknown error"
token =
emit_token(state, [
colorize("!", @red, state),
" ",
dim(truncate(msg, @tool_preview_limit), state)
])
{[closing, token], %{state | line_open: true}}
end
defp render(event, state) do
{closing, state} = end_stream(state)
preview = truncate(inspect(event.type), @tool_preview_limit)
token = emit_token(state, [dim("?", state), dim(preview, state)])
{[closing, token], %{state | line_open: true}}
end
defp end_stream(%{streaming: nil} = state), do: {[], state}
defp end_stream(state) do
closing = if state.line_open, do: "\n", else: []
{closing, %{state | streaming: nil, line_open: false}}
end
defp emit_token(state, content) do
prefix = space_if_open(state)
[prefix | List.wrap(content)]
end
defp space_if_open(%{line_open: true}), do: " "
defp space_if_open(_), do: ""
defp colorize(text, color, %{color: true}), do: color <> text <> @nc
defp colorize(text, _color, _state), do: text
defp dim(text, %{color: true}), do: @dim <> text <> @nc
defp dim(text, _state), do: text
defp model_label("", _state), do: []
defp model_label(model, state), do: [" ", colorize(model, @magenta, state)]
defp short_model(nil), do: ""
defp short_model(model) when is_atom(model), do: short_model(Atom.to_string(model))
defp short_model(model) when is_binary(model) do
model
|> String.replace_prefix("claude-", "")
|> String.replace_prefix("anthropic.", "")
|> String.split("-", trim: true)
|> Enum.take(3)
|> Enum.join("-")
end
defp short_reason(nil), do: ""
defp short_reason(reason) when is_atom(reason), do: short_reason(Atom.to_string(reason))
defp short_reason(reason) when is_binary(reason) do
case String.downcase(reason) do
"tool_use" -> "tool"
"end_turn" -> "end"
other -> other
end
end
defp format_compact_cost(nil), do: ""
defp format_compact_cost(cost) when is_number(cost), do: " $#{Float.round(cost * 1.0, 4)}"
defp truncate(nil, _limit), do: ""
defp truncate(text, limit) when is_binary(text) do
if String.length(text) > limit do
String.slice(text, 0, limit) <> "..."
else
text
end
end
defp truncate(value, limit), do: truncate(inspect(value), limit)
defp format_value(value, limit) when is_binary(value), do: truncate(value, limit)
defp format_value(value, limit), do: truncate(inspect(value), limit)
defp summary_line(state) do
closing = if state.line_open, do: "\n", else: ""
[closing, dim("--- #{state.event_count} events, #{state.tool_count} tools ---\n", state)]
end
end