clacky-ai--openclacky
167 行
5.3 KiB
Ruby
167 行
5.3 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "ui_interface"
|
|
|
|
module Clacky
|
|
# PlainUIController implements UIInterface for non-interactive (--message) mode.
|
|
# Writes human-readable plain text directly to stdout so the caller can capture
|
|
# or pipe the output. No spinners, no TUI — just clean lines.
|
|
class PlainUIController
|
|
include Clacky::UIInterface
|
|
|
|
def initialize(output: $stdout)
|
|
@output = output
|
|
@mutex = Mutex.new
|
|
end
|
|
|
|
# === Output display ===
|
|
|
|
def show_assistant_message(content, files:)
|
|
puts_line(content) unless content.nil? || content.strip.empty?
|
|
files.each { |f| puts_line("📄 File: #{f[:path]}") }
|
|
end
|
|
|
|
def show_tool_call(name, args)
|
|
args_data = args.is_a?(String) ? (JSON.parse(args) rescue args) : args
|
|
|
|
# Special handling for request_user_feedback — display as a readable prompt
|
|
if name.to_s == "request_user_feedback"
|
|
question = args_data.is_a?(Hash) ? (args_data[:question] || args_data["question"]).to_s : ""
|
|
context = args_data.is_a?(Hash) ? (args_data[:context] || args_data["context"]).to_s : ""
|
|
options = args_data.is_a?(Hash) ? (args_data[:options] || args_data["options"]) : nil
|
|
options = Array(options) if options && !options.is_a?(Array)
|
|
|
|
parts = []
|
|
parts << "**Context:** #{context.strip}" if context && !context.strip.empty?
|
|
parts << "**Question:** #{question.strip}"
|
|
if options && !options.empty?
|
|
parts << "**Options:**"
|
|
options.each_with_index { |opt, i| parts << " #{i + 1}. #{opt}" }
|
|
end
|
|
puts_line(parts.join("\n"))
|
|
return
|
|
end
|
|
|
|
display = case name
|
|
when "terminal"
|
|
cmd = args_data.is_a?(Hash) ? (args_data[:command] || args_data["command"]) : args_data
|
|
sid = args_data.is_a?(Hash) ? (args_data[:session_id] || args_data["session_id"]) : nil
|
|
if cmd
|
|
"$ #{cmd}"
|
|
elsif sid
|
|
"$ (session ##{sid})"
|
|
else
|
|
"$ terminal"
|
|
end
|
|
when "write"
|
|
path = args_data.is_a?(Hash) ? (args_data[:path] || args_data["path"]) : args_data
|
|
"Write → #{path}"
|
|
when "edit"
|
|
path = args_data.is_a?(Hash) ? (args_data[:path] || args_data["path"]) : args_data
|
|
"Edit → #{path}"
|
|
else
|
|
label = args_data.is_a?(Hash) ? args_data.map { |k, v| "#{k}=#{v.to_s[0..40]}" }.join(", ") : args_data.to_s[0..80]
|
|
"#{name}(#{label})"
|
|
end
|
|
puts_line("[tool] #{display}")
|
|
end
|
|
|
|
def show_tool_result(result)
|
|
text = result.to_s.strip
|
|
return if text.empty?
|
|
|
|
# Indent multi-line results for readability
|
|
indented = text.lines.map { |l| " #{l}" }.join
|
|
puts_line(indented)
|
|
end
|
|
|
|
def show_tool_error(error)
|
|
msg = error.is_a?(Exception) ? error.message : error.to_s
|
|
puts_line("[error] #{msg}")
|
|
end
|
|
|
|
def show_file_write_preview(path, is_new_file:)
|
|
action = is_new_file ? "create" : "overwrite"
|
|
puts_line("[file] #{action}: #{path}")
|
|
end
|
|
|
|
def show_file_edit_preview(path)
|
|
puts_line("[file] edit: #{path}")
|
|
end
|
|
|
|
def show_file_error(error_message)
|
|
puts_line("[file error] #{error_message}")
|
|
end
|
|
|
|
def show_shell_preview(command)
|
|
puts_line("[shell] #{command}")
|
|
end
|
|
|
|
def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil)
|
|
parts = ["[done] iterations=#{iterations}", "cost=$#{cost.round(4)}"]
|
|
parts << "duration=#{duration.round(1)}s" if duration
|
|
puts_line(parts.join(" "))
|
|
end
|
|
|
|
def append_output(content)
|
|
puts_line(content)
|
|
end
|
|
|
|
# === Status messages ===
|
|
|
|
def show_info(message, prefix_newline: true)
|
|
puts_line("[info] #{message}")
|
|
end
|
|
|
|
def show_warning(message)
|
|
puts_line("[warn] #{message}")
|
|
end
|
|
|
|
def show_error(message, code: nil, top_up_url: nil, raw_message: nil)
|
|
puts_line("[error] #{message}")
|
|
end
|
|
|
|
def show_success(message)
|
|
puts_line("[ok] #{message}")
|
|
end
|
|
|
|
def log(message, level: :info)
|
|
# Only surface errors/warnings; suppress debug noise in plain mode
|
|
puts_line("[#{level}] #{message}") if %i[error warn].include?(level.to_sym)
|
|
end
|
|
|
|
# === Progress (no-ops — no spinner in plain mode) ===
|
|
|
|
def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {}); end
|
|
|
|
# === State updates (no-ops) ===
|
|
|
|
def update_sessionbar(tasks: nil, cost: nil, cost_source: nil, status: nil, latency: nil); end
|
|
def update_todos(todos); end
|
|
def set_working_status; end
|
|
def set_idle_status; end
|
|
|
|
# === Blocking interaction (auto-approve in non-interactive mode) ===
|
|
|
|
def request_confirmation(message, default: true)
|
|
# Should not be reached because permission_mode is forced to auto_approve,
|
|
# but return true as a safety net.
|
|
true
|
|
end
|
|
|
|
# === Input control / Lifecycle (no-ops) ===
|
|
|
|
def clear_input; end
|
|
def set_input_tips(message, type: :info); end
|
|
def stop; end
|
|
|
|
|
|
def puts_line(text)
|
|
@mutex.synchronize do
|
|
@output.puts(text)
|
|
@output.flush
|
|
end
|
|
end
|
|
end
|
|
end
|