Class: Clacky::Channel::ChannelUIController
- Inherits:
-
Object
- Object
- Clacky::Channel::ChannelUIController
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
-
#append_output(content) ⇒ Object
-
#buffer_line(line) ⇒ Object
-
#clear_input ⇒ Object
-
#flush_adapter_pending ⇒ Object
-
#flush_buffer ⇒ Object
-
#flush_buffer_unlocked ⇒ Object
-
#initialize(event, adapter_resolver, status_messages_resolver = nil, process_messages_resolver = nil) ⇒ ChannelUIController
constructor
A new instance of ChannelUIController.
-
#log(message, level: :info) ⇒ Object
-
#request_confirmation(message, default: true) ⇒ Object
Blocking interaction === Not called directly — WebUIController handles the blocking wait and only notifies IM via show_warning.
-
#send_file(path, name = nil) ⇒ Object
-
#send_text(text) ⇒ Object
-
#set_idle_status ⇒ Object
-
#set_input_tips(message, type: :info) ⇒ Object
-
#set_working_status ⇒ Object
-
#show_assistant_message(content, files:, interim: false, created_at: nil) ⇒ Object
-
#show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil, task_id: nil) ⇒ Object
-
#show_diff(old_content, new_content, max_lines: 50) ⇒ Object
-
#show_error(message, code: nil, top_up_url: nil, raw_message: nil) ⇒ Object
-
#show_file_edit_preview(path) ⇒ Object
-
#show_file_error(error_message) ⇒ Object
-
#show_file_write_preview(path, is_new_file:) ⇒ Object
-
#show_info(message, prefix_newline: true) ⇒ Object
Status messages ===.
-
#show_progress(message = nil, prefix_newline: true, output_buffer: nil) ⇒ Object
Progress ===.
-
#show_shell_preview(command) ⇒ Object
-
#show_success(message) ⇒ Object
-
#show_token_usage(token_data) ⇒ Object
-
#show_tool_args(formatted_args) ⇒ Object
-
#show_tool_call(name, args) ⇒ Object
-
#show_tool_error(error) ⇒ Object
-
#show_tool_result(result) ⇒ Object
-
#show_user_message(content) ⇒ Object
Forward WebUI user messages to the IM channel so both sides stay in sync.
-
#show_warning(message) ⇒ Object
-
#stop ⇒ Object
-
#update_message_context(event) ⇒ Object
Update the reply context for the current inbound message.
-
#update_sessionbar(tasks: nil, cost: nil, cost_source: nil, status: nil, latency: nil) ⇒ Object
State updates (no-ops for IM) ===.
-
#update_todos(todos) ⇒ Object
#emit, #phase_end, #phase_start, #request_feedback_with_countdown, #show_feedback_request, #show_goal_status, #show_subagent_end, #show_subagent_start, #show_tool_stdout, #start_progress, #stream_thinking_progress, #update_permission_mode, #with_phase, #with_progress
Constructor Details
#initialize(event, adapter_resolver, status_messages_resolver = nil, process_messages_resolver = nil) ⇒ ChannelUIController
Returns a new instance of ChannelUIController.
36
37
38
39
40
41
42
43
44
45
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 36
def initialize(event, adapter_resolver, status_messages_resolver = nil, process_messages_resolver = nil)
@platform = event[:platform]
@chat_id = event[:chat_id]
@message_id = event[:message_id] @adapter_resolver = adapter_resolver
@status_messages_resolver = status_messages_resolver
@process_messages_resolver = process_messages_resolver
@buffer = []
@mutex = Mutex.new
end
|
Instance Attribute Details
#chat_id ⇒ Object
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
|
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
167
168
169
170
171
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 167
def append_output(content)
return if content.nil? || content.to_s.strip.empty?
send_text(content)
end
|
#buffer_line(line) ⇒ Object
266
267
268
269
270
271
272
273
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 266
def buffer_line(line)
return unless process_messages?
@mutex.synchronize do
@buffer << line
flush_buffer_unlocked if @buffer.size >= BUFFER_FLUSH_SIZE
end
end
|
221
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 221
def clear_input; end
|
#flush_adapter_pending ⇒ Object
286
287
288
289
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 286
def flush_adapter_pending
adapter = @adapter_resolver.call
adapter.flush_pending(@chat_id) if adapter&.respond_to?(:flush_pending)
end
|
#flush_buffer ⇒ Object
275
276
277
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 275
def flush_buffer
@mutex.synchronize { flush_buffer_unlocked }
end
|
#flush_buffer_unlocked ⇒ Object
279
280
281
282
283
284
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 279
def flush_buffer_unlocked
return if @buffer.empty?
send_text(@buffer.join("\n"))
@buffer.clear
end
|
#log(message, level: :info) ⇒ Object
193
194
195
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 193
def log(message, level: :info)
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.
214
215
216
217
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 214
def request_confirmation(message, default: true)
send_text("Confirmation requested (auto-approved): #{message}")
default
end
|
#send_file(path, name = nil) ⇒ Object
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 241
def send_file(path, name = nil)
adapter = @adapter_resolver.call
unless adapter
Clacky::Logger.warn("[ChannelUI] send_file: no live adapter for :#{@platform}")
return nil
end
if adapter.respond_to?(:send_file)
adapter.send_file(@chat_id, path, name: name)
else
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 226
def send_text(text)
text = text.to_s.gsub(/<think>[\s\S]*?<\/think>\n*/i, "").strip
return if text.empty?
adapter = @adapter_resolver.call
unless adapter
Clacky::Logger.warn("[ChannelUI] send_text: no live adapter for :#{@platform}")
return nil
end
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_status ⇒ Object
208
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 208
def set_idle_status; end
|
222
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 222
def set_input_tips(message, type: :info); end
|
#set_working_status ⇒ Object
207
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 207
def set_working_status; end
|
#show_assistant_message(content, files:, interim: false, created_at: nil) ⇒ Object
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
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 70
def show_assistant_message(content, files:, interim: false, created_at: nil)
if interim
return unless process_messages?
flush_buffer
text = content.to_s.strip
send_text(text) unless text.empty?
return
end
flush_buffer
Clacky::Logger.info("[ChannelUI] show_assistant_message files=#{files.size} content_len=#{content.to_s.length}")
text = content.to_s.gsub(/!?\[[^\]]*\]\(file:\/\/[^)]+\)/, "").strip
send_text(text) unless text.empty?
flush_adapter_pending
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, task_id: nil) ⇒ Object
152
153
154
155
156
157
158
159
160
161
162
163
164
165
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 152
def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil, task_id: nil)
flush_buffer
return unless status_messages?
parts = ["Done", "#{iterations} step#{"s" if iterations != 1}"]
if cost && cost > 0 && cost_source
parts << "$#{cost.round(4)}"
end
parts << "#{duration.round(1)}s" if duration
send_text(parts.join(" · "))
flush_adapter_pending
end
|
#show_diff(old_content, new_content, max_lines: 50) ⇒ Object
144
145
146
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 144
def show_diff(old_content, new_content, max_lines: 50)
end
|
#show_error(message, code: nil, top_up_url: nil, raw_message: nil) ⇒ Object
183
184
185
186
187
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 183
def show_error(message, code: nil, top_up_url: nil, raw_message: nil)
text = "Error: #{message}"
text += "\n#{top_up_url}" if top_up_url
send_text(text)
end
|
#show_file_edit_preview(path) ⇒ Object
132
133
134
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 132
def show_file_edit_preview(path)
buffer_line("edit: #{path}")
end
|
#show_file_error(error_message) ⇒ Object
140
141
142
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 140
def show_file_error(error_message)
send_text("File error: #{error_message}")
end
|
#show_file_write_preview(path, is_new_file:) ⇒ Object
127
128
129
130
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 127
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
175
176
177
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 175
def show_info(message, prefix_newline: true)
end
|
#show_progress(message = nil, prefix_newline: true, output_buffer: nil) ⇒ Object
199
200
201
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 199
def show_progress(message = nil, prefix_newline: true, output_buffer: nil)
end
|
#show_shell_preview(command) ⇒ Object
136
137
138
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 136
def show_shell_preview(command)
buffer_line("$ #{command}")
end
|
#show_success(message) ⇒ Object
189
190
191
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 189
def show_success(message)
send_text(message)
end
|
#show_token_usage(token_data) ⇒ Object
148
149
150
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 148
def show_token_usage(token_data)
end
|
123
124
125
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 123
def show_tool_args(formatted_args)
end
|
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 97
def show_tool_call(name, args)
if Clacky::Tools::AskUser.feedback_tool?(name)
args_data = args.is_a?(String) ? (JSON.parse(args) rescue args) : args
questions = Clacky::Tools::AskUser.normalize_questions(args_data)
return if questions.empty?
context = args_data.is_a?(Hash) ? (args_data[:context] || args_data["context"]).to_s : ""
flush_buffer
send_text(Clacky::Tools::AskUser.render_text(questions, context))
return
end
end
|
118
119
120
121
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 118
def show_tool_error(error)
msg = error.is_a?(Exception) ? error.message : error.to_s
send_text("Tool error: #{msg}")
end
|
114
115
116
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 114
def show_tool_result(result)
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.
64
65
66
67
68
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 64
def show_user_message(content)
return if content.nil? || content.to_s.strip.empty?
send_text("[USER] #{content}")
end
|
#show_warning(message) ⇒ Object
179
180
181
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 179
def show_warning(message)
send_text("Warning: #{message}")
end
|
#stop ⇒ Object
223
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 223
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.
53
54
55
56
57
58
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 53
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) ===
205
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 205
def update_sessionbar(tasks: nil, cost: nil, cost_source: nil, status: nil, latency: nil); end
|
#update_todos(todos) ⇒ Object
206
|
# File 'lib/clacky/server/channel/channel_ui_controller.rb', line 206
def update_todos(todos); end
|