Class: Crimson::OutputHandler
- Inherits:
-
Object
- Object
- Crimson::OutputHandler
- Defined in:
- lib/crimson/output_handler.rb
Overview
Streaming output handler with spinner, tool call logging, and usage statistics. Subscribes to agent events to provide real-time terminal feedback.
Constant Summary collapse
- RENDER_INTERVAL =
Interval in seconds between render flushes.
0.05- SPINNER_FRAMES =
Spinner animation frame characters.
["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"].freeze
- TOOL_STYLES =
Visual styles for known tools (prefix color and label).
{ "read_file" => { prefix: "→Read", color: :blue }, "write_file" => { prefix: "→Write", color: :green }, "edit_file" => { prefix: "→Edit", color: :yellow }, "run_command" => { prefix: "$", color: :bright_white }, "search_files" => { prefix: "✱Search", color: :cyan }, "glob" => { prefix: "✱Glob", color: :cyan }, "list_directory" => { prefix: "→List", color: :cyan } }.freeze
- TOOL_ARG_EXTRACTORS =
Extractors to pull display-relevant arguments from tool call argument hashes.
{ "read_file" => ->(a) { a["path"] || a[:path] }, "write_file" => ->(a) { a["path"] || a[:path] }, "edit_file" => ->(a) { a["path"] || a[:path] }, "run_command" => ->(a) { a["command"] || a[:command] }, "search_files" => ->(a) { a["pattern"] || a[:pattern] }, "list_directory" => ->(a) { a["path"] || a[:path] }, "glob" => ->(a) { a["pattern"] || a[:pattern] } }.freeze
Instance Method Summary collapse
-
#attach(agent) ⇒ void
Subscribe to events on the given agent for output rendering.
-
#initialize ⇒ OutputHandler
constructor
A new instance of OutputHandler.
Constructor Details
#initialize ⇒ OutputHandler
Returns a new instance of OutputHandler.
37 38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/crimson/output_handler.rb', line 37 def initialize @pastel = Pastel.new @spinner_active = false @first_token = false @render_buffer = String.new @render_thread = nil @render_mutex = Mutex.new @spinner_thread = nil @seen_tool_calls = Set.new @thinking_start = nil @run_start = nil end |
Instance Method Details
#attach(agent) ⇒ void
This method returns an undefined value.
Subscribe to events on the given agent for output rendering.
53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 |
# File 'lib/crimson/output_handler.rb', line 53 def attach(agent) agent.on(Agent::Events::AGENT_START) do @first_token = false Formatter.reset @thinking_start = Time.now @run_start = Time.now start_spinner end agent.on(Agent::Events::MESSAGE_UPDATE) do |_event, delta:, **| unless @first_token stop_spinner @first_token = true if @thinking_start elapsed = format("%.1fs", Time.now - @thinking_start) puts @pastel.dim("+ Thought: #{elapsed}") @thinking_start = nil end end @render_mutex.synchronize { @render_buffer << delta } start_render_thread unless @render_thread&.alive? end agent.on(Agent::Events::TOOL_EXECUTION_START) do |_event, tool_name:, args:, tool_call_id:, **| next if @seen_tool_calls.include?(tool_call_id) @seen_tool_calls << tool_call_id stop_spinner $stdout.write("\r\e[2K") $stdout.flush log_tool_call(tool_name, args) end agent.on(Agent::Events::TOOL_EXECUTION_END) do |_event, result:, is_error:, tool_call_id:, **| next if tool_call_id && @seen_tool_calls.include?("#{tool_call_id}_end") @seen_tool_calls << "#{tool_call_id}_end" if tool_call_id next unless is_error truncated = truncate(result.to_s, 120) puts @pastel.red(" ✗ #{truncated}") end agent.on(Agent::Events::TOOL_EXECUTION_UPDATE) do |_event, tool_name:, partial_result:, **| next unless tool_name == "run_command" flush_render_buffer $stdout.write("\r\e[2K #{@pastel.dim(partial_result)}") $stdout.flush end agent.on(Agent::Events::TURN_START) do |_event, active_skills: []| conditional = active_skills.reject { |s| s == "coding" } unless conditional.empty? stop_spinner puts @pastel.dim("+ #{conditional.join(", ")}") end unless @first_token @thinking_start = Time.now start_spinner end end agent.on(Agent::Events::AGENT_END) do stop_spinner flush_render_buffer(final: true) @seen_tool_calls.clear elapsed = @run_start ? format_elapsed(Time.now - @run_start) : "" usage = agent.token_usage parts = [] if usage[:total] > 0 cost = agent.cost_tracker.total_cost cost_str = cost > 0 ? " ($#{format("%.4f", cost)})" : "" parts << "tokens: #{usage[:prompt]}↑ #{usage[:completion]}↓ = #{usage[:total]}#{cost_str}" end parts << "time: #{elapsed}" unless elapsed.empty? puts @pastel.dim("\n #{parts.join(" · ")}") unless parts.empty? end end |