clacky-ai--openclacky
172 行
7.1 KiB
Ruby
172 行
7.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require "securerandom"
|
|
|
|
module Clacky
|
|
# UIInterface defines the standard interface between Agent/CLI and UI implementations.
|
|
# All UI controllers (UIController, JsonUIController) must implement these methods.
|
|
module UIInterface
|
|
# === Output display ===
|
|
# @param content [String] text portion of the assistant reply (file:// links stripped)
|
|
# @param files [Array<Hash>] extracted file refs: [{ name:, path:, inline: }]
|
|
def show_assistant_message(content, files:); end
|
|
def show_feedback_request(question, context, options); end
|
|
def show_tool_call(name, args); end
|
|
def show_tool_result(result); end
|
|
def show_tool_stdout(lines); end
|
|
def show_tool_error(error); end
|
|
def show_tool_args(formatted_args); end
|
|
def show_file_write_preview(path, is_new_file:); end
|
|
def show_file_edit_preview(path); end
|
|
def show_file_error(error_message); end
|
|
def show_shell_preview(command); end
|
|
def show_diff(old_content, new_content, max_lines: 50); end
|
|
def show_token_usage(token_data); end
|
|
def show_complete(iterations:, cost:, duration: nil, cache_stats: nil, awaiting_user_feedback: false, cost_source: nil); end
|
|
def append_output(content); end
|
|
|
|
# === Status messages ===
|
|
def show_info(message, prefix_newline: true); end
|
|
def show_warning(message); end
|
|
def show_error(message, code: nil, top_up_url: nil, raw_message: nil); end
|
|
def show_success(message); end
|
|
def log(message, level: :info); end
|
|
|
|
# === Progress ===
|
|
# Unified progress indicator with type-based display customization.
|
|
# progress_type: "thinking" | "retrying" | "idle_compress" | custom
|
|
# phase: "active" | "done"
|
|
# metadata: extensible hash (e.g., {attempt: 3, total: 10} for retries)
|
|
def show_progress(message = nil, prefix_newline: true, progress_type: "thinking", phase: "active", metadata: {}); end
|
|
|
|
# Update the live "thinking" progress with streamed token counts.
|
|
# This is *purely decorative*: it must NEVER start a new progress
|
|
# indicator. If no thinking progress is currently active (e.g. during
|
|
# idle compression, where only a quiet "Compressing..." progress is
|
|
# live), the call is a no-op. UI2 overrides this; other UIs delegate
|
|
# to show_progress.
|
|
def stream_thinking_progress(input_tokens:, output_tokens:)
|
|
show_progress(
|
|
progress_type: "thinking",
|
|
phase: "active",
|
|
metadata: { input_tokens: input_tokens, output_tokens: output_tokens }
|
|
)
|
|
end
|
|
|
|
# === Progress (v2: owned handles) ===
|
|
#
|
|
# Start a new progress indicator and return an owned handle. The caller
|
|
# is responsible for finishing it — use +with_progress+ (below) whenever
|
|
# possible to get ensure-based auto-close.
|
|
#
|
|
# @param message [String, nil] Initial progress message (nil picks a random thinking verb).
|
|
# @param style [Symbol] :primary (foreground, yellow, bumps sessionbar)
|
|
# or :quiet (background, gray, no sessionbar change).
|
|
# @param quiet_on_fast_finish [Boolean] When true, a finish under
|
|
# FAST_FINISH_THRESHOLD_SECONDS removes the progress line entirely
|
|
# (preferred for per-tool wrappers so fast tools don't leave a
|
|
# permanent "Executing foo… (0s)" log line). The default
|
|
# implementation ignores this flag — it only affects the native
|
|
# UI2::UIController + ProgressHandle path.
|
|
# @return [#update, #finish, #cancel] a ProgressHandle-like object.
|
|
#
|
|
# Default implementation degrades gracefully to the old show_progress API
|
|
# so UI implementations that haven't migrated still behave correctly.
|
|
def start_progress(message: nil, style: :primary, quiet_on_fast_finish: false)
|
|
_ = quiet_on_fast_finish # default impl doesn't honor fast-collapse
|
|
progress_type = style == :primary ? "thinking" : "idle_compress"
|
|
show_progress(message, progress_type: progress_type, phase: "active")
|
|
LegacyProgressHandleAdapter.new(self, progress_type: progress_type)
|
|
end
|
|
|
|
# Run the given block with a progress indicator active. The handle is
|
|
# always finished in an +ensure+ block — exceptions (including
|
|
# AgentInterrupted) cannot leave the ticker or entry orphaned.
|
|
#
|
|
# @yieldparam handle the progress handle
|
|
def with_progress(message: nil, style: :primary, quiet_on_fast_finish: false)
|
|
handle = start_progress(
|
|
message: message,
|
|
style: style,
|
|
quiet_on_fast_finish: quiet_on_fast_finish
|
|
)
|
|
begin
|
|
yield handle
|
|
ensure
|
|
handle.finish
|
|
end
|
|
end
|
|
|
|
# Minimal adapter that lets UIs without a native ProgressHandle still
|
|
# participate in the new +with_progress+ API by delegating to the old
|
|
# +show_progress(phase: ...)+ contract. UI2::UIController overrides
|
|
# +start_progress+ directly with a native ProgressHandle, so this
|
|
# adapter is only used by plain/json/web/channel UIs.
|
|
class LegacyProgressHandleAdapter
|
|
def initialize(ui, progress_type:)
|
|
@ui = ui
|
|
@progress_type = progress_type
|
|
@closed = false
|
|
end
|
|
|
|
def update(message: nil, metadata: nil)
|
|
return if @closed
|
|
@ui.show_progress(message, progress_type: @progress_type, phase: "active", metadata: metadata || {})
|
|
end
|
|
|
|
def finish(final_message: nil)
|
|
return if @closed
|
|
@closed = true
|
|
@ui.show_progress(final_message, progress_type: @progress_type, phase: "done")
|
|
end
|
|
alias_method :cancel, :finish
|
|
end
|
|
|
|
# === State updates ===
|
|
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 ===
|
|
def request_confirmation(message, default: true); end
|
|
|
|
# Auto-approve countdown for request_user_feedback. Shows a live countdown
|
|
# and lets the user press a key to take over and answer. Returns :timeout
|
|
# when no one intervenes (agent should auto-decide and continue), or a
|
|
# feedback string / "" when the user steps in. Non-interactive UIs (web,
|
|
# json, channel) have no human watching a TTY, so they default to :timeout.
|
|
def request_feedback_with_countdown(seconds: 10)
|
|
:timeout
|
|
end
|
|
|
|
# === Input control (CLI layer) ===
|
|
def clear_input; end
|
|
def set_input_tips(message, type: :info); end
|
|
|
|
# === Path redaction (for encrypted brand skill tmpdirs) ===
|
|
# === Lifecycle ===
|
|
def stop(clear_screen: false); end
|
|
|
|
# === Phase grouping (optional, web UI uses this to fold subagent runs) ===
|
|
# Begin a logical phase. Events emitted between phase_start and phase_end
|
|
# carry the phase_id so the UI can group them visually.
|
|
# Returns the phase_id (caller is responsible for passing it to phase_end).
|
|
def phase_start(kind:, label: nil)
|
|
SecureRandom.uuid
|
|
end
|
|
|
|
def phase_end(phase_id, summary: nil); end
|
|
|
|
# Run block within a phase. Always closes via ensure.
|
|
def with_phase(kind:, label: nil)
|
|
pid = phase_start(kind: kind, label: label)
|
|
begin
|
|
yield pid
|
|
ensure
|
|
phase_end(pid)
|
|
end
|
|
end
|
|
end
|
|
end
|