Class: Clacky::Channel::ChannelUIController

Inherits:
Object
  • Object
show all
Includes:
UIInterface
Defined in:
lib/clacky/server/channel/channel_ui_controller.rb

Overview

ChannelUIController implements UIInterface for IM platform sessions. It is registered as a subscriber on WebUIController so that every agent output event is forwarded here and sent back to the IM platform.

Design notes:

  • Tool calls / results / diffs / token usage are intentionally suppressed to keep IM chat clean. Only high-signal events are forwarded.

  • Buffering: file/shell previews accumulate in a buffer and are flushed as one message before the next assistant message, avoiding flooding.

  • request_confirmation is not invoked directly on this class — the Web UI handles the blocking wait and only sends show_warning notifications.

Constant Summary collapse

BUFFER_FLUSH_SIZE =

flush early when buffer is large

5

Instance Attribute Summary collapse

Instance Method Summary collapse

Methods included from UIInterface

#show_tool_stdout, #start_progress, #with_progress

Constructor Details

#initialize(event, adapter) ⇒ ChannelUIController

Returns a new instance of ChannelUIController.



25
26
27
28
29
30
31
32
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 25

def initialize(event, adapter)
  @platform   = event[:platform]
  @chat_id    = event[:chat_id]
  @message_id = event[:message_id]  # original message to reply under
  @adapter    = adapter
  @buffer     = []
  @mutex      = Mutex.new
end

Instance Attribute Details

#chat_idObject (readonly)

Returns the value of attribute chat_id.



23
24
25
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 23

def chat_id
  @chat_id
end

#platformObject (readonly)

Returns the value of attribute platform.



23
24
25
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 23

def platform
  @platform
end

Instance Method Details

#append_output(content) ⇒ Object



125
126
127
128
129
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 125

def append_output(content)
  return if content.nil? || content.to_s.strip.empty?

  send_text(content)
end

#buffer_line(line) ⇒ Object



204
205
206
207
208
209
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 204

def buffer_line(line)
  @mutex.synchronize do
    @buffer << line
    flush_buffer_unlocked if @buffer.size >= BUFFER_FLUSH_SIZE
  end
end

#clear_inputObject

Input control / lifecycle (no-ops) ===



177
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 177

def clear_input; end

#flush_bufferObject



211
212
213
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 211

def flush_buffer
  @mutex.synchronize { flush_buffer_unlocked }
end

#flush_buffer_unlockedObject



215
216
217
218
219
220
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 215

def flush_buffer_unlocked
  return if @buffer.empty?

  send_text(@buffer.join("\n"))
  @buffer.clear
end

#log(message, level: :info) ⇒ Object



149
150
151
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 149

def log(message, level: :info)
  # Suppress
end

#request_confirmation(message, default: true) ⇒ Object

Blocking interaction ===

Not called directly — WebUIController handles the blocking wait and only notifies IM via show_warning. Implemented as auto-approve as a safety fallback in case this is ever called directly.



170
171
172
173
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 170

def request_confirmation(message, default: true)
  send_text("Confirmation requested (auto-approved): #{message}")
  default
end

#send_file(path, name = nil) ⇒ Object



192
193
194
195
196
197
198
199
200
201
202
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 192

def send_file(path, name = nil)
  if @adapter.respond_to?(:send_file)
    @adapter.send_file(@chat_id, path, name: name)
  else
    # Fallback for adapters that don't support file sending
    send_text("File: #{name || File.basename(path)}\n#{path}")
  end
rescue StandardError => e
  Clacky::Logger.warn("[ChannelUI] send_file failed (#{@platform}/#{@chat_id}): #{e.message}")
  send_text("Failed to send file: #{File.basename(path)}\nError: #{e.message}")
end

#send_text(text) ⇒ Object



182
183
184
185
186
187
188
189
190
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 182

def send_text(text)
  text = text.to_s.gsub(/<think>[\s\S]*?<\/think>\n*/i, "").strip
  return if text.empty?

  @adapter.send_text(@chat_id, text, reply_to: @message_id)
rescue StandardError => e
  Clacky::Logger.warn("[ChannelUI] send_text failed", platform: @platform, chat_id: @chat_id, error: e)
  nil
end

#set_idle_statusObject



164
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 164

def set_idle_status; end

#set_input_tips(message, type: :info) ⇒ Object



178
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 178

def set_input_tips(message, type: :info); end

#set_working_statusObject



163
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 163

def set_working_status; end

#show_assistant_message(content, files:) ⇒ Object



57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 57

def show_assistant_message(content, files:)
  flush_buffer
  Clacky::Logger.info("[ChannelUI] show_assistant_message files=#{files.size} content_len=#{content.to_s.length}")
  # Strip file:// markdown links from the text sent to IM channels —
  # the actual files are delivered via send_file() below, so the
  # raw markdown links would just be noise in the chat.
  text = content.to_s.gsub(/!?\[[^\]]*\]\(file:\/\/[^)]+\)/, "").strip
  send_text(text) unless text.empty?
  files.each do |f|
    Clacky::Logger.info("[ChannelUI] sending file path=#{f[:path].inspect} name=#{f[:name].inspect}")
    send_file(f[:path], f[:name])
  end
end

#show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil) ⇒ Object



113
114
115
116
117
118
119
120
121
122
123
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 113

def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil)
  flush_buffer
  parts = ["Done", "#{iterations} step#{"s" if iterations != 1}"]
  # Only show cost when pricing source is known (model matched pricing table).
  # Unknown models return nil — skip to avoid misleading numbers.
  if cost && cost > 0 && cost_source
    parts << "$#{cost.round(4)}"
  end
  parts << "#{duration.round(1)}s" if duration
  send_text(parts.join(" · "))
end

#show_diff(old_content, new_content, max_lines: 50) ⇒ Object



105
106
107
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 105

def show_diff(old_content, new_content, max_lines: 50)
  # Diffs are too verbose for IM — suppress
end

#show_error(message) ⇒ Object



141
142
143
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 141

def show_error(message)
  send_text("Error: #{message}")
end

#show_file_edit_preview(path) ⇒ Object



93
94
95
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 93

def show_file_edit_preview(path)
  buffer_line("edit: #{path}")
end

#show_file_error(error_message) ⇒ Object



101
102
103
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 101

def show_file_error(error_message)
  send_text("File error: #{error_message}")
end

#show_file_write_preview(path, is_new_file:) ⇒ Object



88
89
90
91
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 88

def show_file_write_preview(path, is_new_file:)
  action = is_new_file ? "create" : "overwrite"
  buffer_line("#{action}: #{path}")
end

#show_info(message, prefix_newline: true) ⇒ Object

Status messages ===



133
134
135
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 133

def show_info(message, prefix_newline: true)
  # Suppress informational noise in IM
end

#show_progress(message = nil, prefix_newline: true, output_buffer: nil) ⇒ Object

Progress ===



155
156
157
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 155

def show_progress(message = nil, prefix_newline: true, output_buffer: nil)
  # Suppress — progress spinner has no IM equivalent
end

#show_shell_preview(command) ⇒ Object



97
98
99
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 97

def show_shell_preview(command)
  buffer_line("$ #{command}")
end

#show_success(message) ⇒ Object



145
146
147
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 145

def show_success(message)
  send_text(message)
end

#show_token_usage(token_data) ⇒ Object



109
110
111
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 109

def show_token_usage(token_data)
  # Suppress
end

#show_tool_args(formatted_args) ⇒ Object



84
85
86
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 84

def show_tool_args(formatted_args)
  # Suppress
end

#show_tool_call(name, args) ⇒ Object



71
72
73
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 71

def show_tool_call(name, args)
  # Suppress — too noisy for IM
end

#show_tool_error(error) ⇒ Object



79
80
81
82
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 79

def show_tool_error(error)
  msg = error.is_a?(Exception) ? error.message : error.to_s
  send_text("Tool error: #{msg}")
end

#show_tool_result(result) ⇒ Object



75
76
77
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 75

def show_tool_result(result)
  # Suppress — too noisy for IM
end

#show_user_message(content) ⇒ Object

Forward WebUI user messages to the IM channel so both sides stay in sync. Prefixed with the product/user context so it’s clear who sent it.



51
52
53
54
55
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 51

def show_user_message(content)
  return if content.nil? || content.to_s.strip.empty?

  send_text("[USER] #{content}")
end

#show_warning(message) ⇒ Object



137
138
139
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 137

def show_warning(message)
  send_text("Warning: #{message}")
end

#stopObject



179
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 179

def stop; end

#update_message_context(event) ⇒ Object

Update the reply context for the current inbound message. Called at the start of each route_message so replies are threaded correctly. Also updates chat_id — a session may span multiple chats (e.g. same user in both a direct message and a group), and each inbound event dictates where outbound replies should be routed.

Parameters:

  • event (Hash)

    inbound event with :message_id and :chat_id



40
41
42
43
44
45
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 40

def update_message_context(event)
  @mutex.synchronize do
    @message_id = event[:message_id]
    @chat_id    = event[:chat_id] if event[:chat_id]
  end
end

#update_sessionbar(tasks: nil, cost: nil, cost_source: nil, status: nil, latency: nil) ⇒ Object

State updates (no-ops for IM) ===



161
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 161

def update_sessionbar(tasks: nil, cost: nil, cost_source: nil, status: nil, latency: nil); end

#update_todos(todos) ⇒ Object



162
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 162

def update_todos(todos); end