Class: Clacky::Server::HistoryCollector

Inherits:
Object
  • Object
show all
Defined in:
lib/clacky/server/http_server.rb

Overview

Lightweight UI collector used by api_session_messages to capture events emitted by Agent#replay_history without broadcasting over WebSocket. Implements the same show_* interface as WebUIController.

Instance Method Summary collapse

Constructor Details

#initialize(session_id, events) ⇒ HistoryCollector

Returns a new instance of HistoryCollector.



36
37
38
39
40
# File 'lib/clacky/server/http_server.rb', line 36

def initialize(session_id, events)
  @session_id = session_id
  @events     = events
  @replay_phase_id = nil
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(name, *args, **kwargs) ⇒ Object

Ignore all other UI methods (progress, errors, etc.) during history replay



158
# File 'lib/clacky/server/http_server.rb', line 158

def method_missing(name, *args, **kwargs); end

Instance Method Details

#emit(type, **data) ⇒ Object

Custom extension events recorded on messages (Agent#emit_event). Must be explicit: method_missing below would otherwise swallow them.



153
154
155
# File 'lib/clacky/server/http_server.rb', line 153

def emit(type, **data)
  @events << { type: type, session_id: @session_id }.merge(data)
end

#phase_end(phase_id, summary: nil) ⇒ Object



56
57
58
59
60
61
# File 'lib/clacky/server/http_server.rb', line 56

def phase_end(phase_id, summary: nil)
  @replay_phase_id = nil if @replay_phase_id == phase_id
  ev = { type: "phase_end", session_id: @session_id, phase_id: phase_id }
  ev[:summary] = summary if summary
  @events << ev
end

#phase_start(kind:, label: nil, concurrent: false) ⇒ Object

Group replayed subagent events into a foldable phase card, mirroring the real-time WebUIController#phase_start/phase_end so the frontend nests them instead of dropping them into the outer stream. Replay is a single sequential pass, so an instance variable stands in for the thread-local phase id the live path uses.



47
48
49
50
51
52
53
54
# File 'lib/clacky/server/http_server.rb', line 47

def phase_start(kind:, label: nil, concurrent: false)
  pid = SecureRandom.uuid
  @replay_phase_id = pid
  ev = { type: "phase_start", session_id: @session_id, phase_id: pid, kind: kind.to_s }
  ev[:label] = label if label
  @events << ev
  pid
end

#respond_to_missing?(name, include_private = false) ⇒ Boolean

Returns:

  • (Boolean)


159
# File 'lib/clacky/server/http_server.rb', line 159

def respond_to_missing?(name, include_private = false); true; end

#show_assistant_message(content, files:, interim: false, created_at: nil) ⇒ Object



99
100
101
102
103
104
105
106
107
# File 'lib/clacky/server/http_server.rb', line 99

def show_assistant_message(content, files:, interim: false, created_at: nil)
  return if content.nil? || content.to_s.strip.empty?

  # Rewrite local image paths to /api/local-image proxy URLs for browser rendering
  rewritten = Utils::FileProcessor.rewrite_local_image_urls(content.to_s)
  ev = { type: "assistant_message", session_id: @session_id, content: rewritten }
  ev[:created_at] = created_at if created_at
  @events << stamp(ev)
end

#show_feedback_request(question, context, options, questions: nil) ⇒ Object



136
137
138
139
140
# File 'lib/clacky/server/http_server.rb', line 136

def show_feedback_request(question, context, options, questions: nil)
  @events << { type: "request_feedback", session_id: @session_id,
               question: question, context: context, options: options,
               questions: questions || [] }
end

#show_subagent_endObject



147
148
149
# File 'lib/clacky/server/http_server.rb', line 147

def show_subagent_end
  @events << stamp({ type: "subagent_end", session_id: @session_id })
end

#show_subagent_start(skill: nil, iterations: nil, cost_usd: nil) ⇒ Object



142
143
144
145
# File 'lib/clacky/server/http_server.rb', line 142

def show_subagent_start(skill: nil, iterations: nil, cost_usd: nil)
  @events << stamp({ type: "subagent_start", session_id: @session_id,
                     skill: skill, iterations: iterations, cost_usd: cost_usd })
end

#show_token_usage(token_data) ⇒ Object



130
131
132
133
134
# File 'lib/clacky/server/http_server.rb', line 130

def show_token_usage(token_data)
  return unless token_data.is_a?(Hash)

  @events << { type: "token_usage", session_id: @session_id }.merge(token_data)
end

#show_tool_call(name, args) ⇒ Object



109
110
111
112
113
# File 'lib/clacky/server/http_server.rb', line 109

def show_tool_call(name, args)
  args_data = args.is_a?(String) ? (JSON.parse(args) rescue args) : args
  summary   = tool_call_summary(name, args_data)
  @events << stamp({ type: "tool_call", session_id: @session_id, name: name, args: args_data, summary: summary })
end

#show_tool_result(result) ⇒ Object



126
127
128
# File 'lib/clacky/server/http_server.rb', line 126

def show_tool_result(result)
  @events << stamp({ type: "tool_result", session_id: @session_id, result: result })
end

#show_user_message(content, task_id: nil, created_at: nil, files: [], editable: true, skill_command: nil, skill_command_display: nil, references: []) ⇒ Object



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
# File 'lib/clacky/server/http_server.rb', line 68

def show_user_message(content, task_id: nil, created_at: nil, files: [], editable: true, skill_command: nil, skill_command_display: nil, references: [])
  ev = { type: "history_user_message", session_id: @session_id, content: content }
  ev[:task_id] = task_id if task_id
  ev[:created_at] = created_at if created_at
  ev[:skill_command] = skill_command if skill_command
  ev[:skill_command_display] = skill_command_display if skill_command_display
  ev[:editable] = false unless editable
  ev[:references] = references unless Array(references).empty?
  rendered = Array(files).filter_map do |f|
    url  = f[:data_url] || f["data_url"]
    name = f[:name]     || f["name"]
    path = f[:path]     || f["path"]
    type = f[:type]     || f["type"] || ""

    if url
      url
    elsif type.to_s == "image" && path && File.exist?(path.to_s)
      # Serve via the /api/local-image proxy instead of inlining a base64
      # data URL. Inlining forced a synchronous disk-read + full base64
      # encode + downscale on every history replay (2-3s lag for sessions
      # with downgraded text-model images). The proxy lets the browser
      # lazy-load + cache the image, keeping the replay response tiny.
      "/api/local-image?path=#{CGI.escape(path.to_s)}&v=#{File.mtime(path.to_s).to_i}"
    elsif name
      type.to_s == "image" ? "expired:#{name}" : "pdf:#{name}"
    end
  end
  ev[:images] = rendered unless rendered.empty?
  @events << ev
end