Class: Agentilda::UI::Line

Inherits:
Object
  • Object
show all
Defined in:
lib/agentilda/ui.rb

Overview

One item's spinner line, and the same news written to the log.

These are two readers of one story and used to be told it separately: the spinner got the phrase, the log got a start and a finish, and an animated run wrote nothing about what any agent was actually doing. A Transcript::Progress arrives here several times a second; the spinner is redrawn every time, and the log takes a line only when the phrase itself changes, which is a few dozen times an agent.

Instance Method Summary collapse

Constructor Details

#initialize(fields: {}, spinner: nil, mark: "", timeout: nil) ⇒ Line

Returns a new instance of Line.

Parameters:

  • fields (Hash) (defaults to: {})

    plan, status and agent, for the log's columns

  • spinner (TTY::Spinner, nil) (defaults to: nil)

    nil where nothing is being drawn

  • mark (String) (defaults to: "")

    what the spinner says once the work succeeds

  • timeout (Integer, nil) (defaults to: nil)

    seconds until the executor abandons this agent; drawn as a countdown on the line, nil draws nothing



65
66
67
68
69
70
71
72
73
74
# File 'lib/agentilda/ui.rb', line 65

def initialize(fields: {}, spinner: nil, mark: "", timeout: nil)
  @fields = fields
  @spinner = spinner
  @mark = mark
  @timeout = timeout
  @started = UI.monotonic
  @phrase = nil
  @pid = nil
  @ticker = nil
end

Instance Method Details

#aliveString

Returns the same, as the log and the report write it.

Returns:

  • (String)

    the same, as the log and the report write it



80
# File 'lib/agentilda/ui.rb', line 80

def alive = "#{seconds.round}s"

#call(update) ⇒ void

This method returns an undefined value.

Parameters:



163
164
165
166
167
168
169
170
171
172
173
174
# File 'lib/agentilda/ui.rb', line 163

def call(update)
  if update.respond_to?(:pid) && update.pid && update.pid != @pid
    @pid = update.pid
    @spinner&.update(pid: identity)
    note("claude is pid #{@pid}")
  end
  @spinner&.update(meter: UI.meter(update), activity: UI.said(update.activity))
  return if update.activity.nil? || update.activity == @phrase

  @phrase = update.activity
  note(update.activity)
end

#donevoid

This method returns an undefined value.



147
148
149
150
151
# File 'lib/agentilda/ui.rb', line 147

def done
  stop_ticker
  @spinner&.success(@mark)
  note("finished after #{alive}")
end

#failed(reason) ⇒ void

This method returns an undefined value.

Parameters:

  • reason (String)


155
156
157
158
159
# File 'lib/agentilda/ui.rb', line 155

def failed(reason)
  stop_ticker
  @spinner&.error(UI.paint(reason, :red))
  note("failed after #{alive}: #{reason}")
end

#identityString

What sits between the agent's name and its activity: the claude process and the round, [36123, round 01], so a line on the screen can be matched to a process in ps and a row in the report. The pid is unknowable until the child has been spawned and found, so it reads until then; a caller with neither fact gets nothing at all.

Returns:

  • (String)


138
139
140
141
142
143
144
# File 'lib/agentilda/ui.rb', line 138

def identity
  round = @fields[:round]
  return "" if round.nil? && @pid.nil?

  inner = [(@pid || "").to_s, ("round #{round}" if round)].compact.join(", ")
  UI.paint("[#{inner}]", :bright_black)
end

#note(message) ⇒ void

This method returns an undefined value.

Parameters:

  • message (String)


84
# File 'lib/agentilda/ui.rb', line 84

def note(message) = UI.log(message, **@fields, seconds:)

#remainingInteger?

What remains on this agent's clock, or nil when it has none. Floored at zero: the executor's kill and this thread's wake-up race by up to a second, and a line reading -0:01 accuses the wrong party.

Returns:

  • (Integer, nil)


98
99
100
101
102
# File 'lib/agentilda/ui.rb', line 98

def remaining
  return nil unless @timeout

  [@timeout - seconds, 0].max.round
end

#secondsFloat

Returns seconds this agent has been alive.

Returns:

  • (Float)

    seconds this agent has been alive



77
# File 'lib/agentilda/ui.rb', line 77

def seconds = UI.monotonic - @started

#startvoid

This method returns an undefined value.



87
88
89
90
91
# File 'lib/agentilda/ui.rb', line 87

def start
  @spinner&.update(pid: identity)
  tick
  note("started")
end

#stop_tickervoid

This method returns an undefined value.



126
127
128
129
# File 'lib/agentilda/ui.rb', line 126

def stop_ticker
  @ticker&.kill
  @ticker = nil
end

#tickvoid

This method returns an undefined value.

The countdown, redrawn once a second on its own thread. Progress updates cannot drive it — they arrive only while the agent is talking, and a stalled agent is exactly when the clock matters most.



109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
# File 'lib/agentilda/ui.rb', line 109

def tick
  return unless @spinner && @timeout

  @spinner.update(timer: UI.countdown(remaining))
  @ticker ||= Thread.new do
    loop do
      sleep(1)
      left = remaining
      @spinner.update(timer: UI.countdown(left))
      break unless left.positive?
    end
  rescue
    # A dying spinner must not take the round down with it.
  end
end

#to_procProc

So a caller can pass this straight on as a block: Executor yields to it from the thread it reads the agent's stream on.

Returns:

  • (Proc)


180
# File 'lib/agentilda/ui.rb', line 180

def to_proc = method(:call).to_proc