agentilda 1.0.3 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/agentilda.gemspec +10 -6
- data/agents/hansolo-reviewer.md +42 -13
- data/agents/lando-broker.md +40 -46
- data/agents/leah-researcher.md +36 -58
- data/agents/luke-backend.md +49 -61
- data/agents/palpatine-planner.md +39 -21
- data/agents/rey-frontend.md +44 -86
- data/agents/yoda-writer.md +38 -36
- data/bin/setup +26 -0
- data/exe/agentilda +13 -92
- data/exe/tilda-rs +0 -0
- data/lib/agentilda/adoption.rb +7 -2
- data/lib/agentilda/agent.rb +40 -99
- data/lib/agentilda/agents.rb +122 -0
- data/lib/agentilda/board.rb +91 -0
- data/lib/agentilda/brief.rb +54 -5
- data/lib/agentilda/child.rb +92 -0
- data/lib/agentilda/cli/agents/subcommands/describe.rb +8 -4
- data/lib/agentilda/cli/base.rb +46 -6
- data/lib/agentilda/cli/create/create.rb +46 -19
- data/lib/agentilda/cli/docs/docs.rb +6 -2
- data/lib/agentilda/cli/index/index.rb +6 -4
- data/lib/agentilda/cli/linear/linear.rb +3 -2
- data/lib/agentilda/cli/linear/subcommands/import.rb +26 -15
- data/lib/agentilda/cli/mail/mail.rb +31 -0
- data/lib/agentilda/cli/mail/subcommands/read.rb +43 -0
- data/lib/agentilda/cli/mail/subcommands/send.rb +43 -0
- data/lib/agentilda/cli/resync/subcommands/dirs.rb +6 -4
- data/lib/agentilda/cli/resync/subcommands/prs.rb +21 -13
- data/lib/agentilda/cli/run/run.rb +207 -99
- data/lib/agentilda/cli/unblock/unblock.rb +24 -13
- data/lib/agentilda/cli/worktree/worktree.rb +85 -0
- data/lib/agentilda/cli.rb +43 -0
- data/lib/agentilda/clock.rb +114 -0
- data/lib/agentilda/console.rb +157 -0
- data/lib/agentilda/control.rb +63 -0
- data/lib/agentilda/creator.rb +7 -2
- data/lib/agentilda/dashboard.rb +219 -0
- data/lib/agentilda/dev_work.rb +2 -0
- data/lib/agentilda/diagram.rb +12 -7
- data/lib/agentilda/dispatcher.rb +646 -0
- data/lib/agentilda/documentation.rb +14 -6
- data/lib/agentilda/executor.rb +362 -204
- data/lib/agentilda/feature.rb +14 -135
- data/lib/agentilda/frontmatter.rb +4 -2
- data/lib/agentilda/github.rb +19 -11
- data/lib/agentilda/index.rb +11 -11
- data/lib/agentilda/keyboard.rb +76 -8
- data/lib/agentilda/launcher.rb +69 -0
- data/lib/agentilda/ledger.rb +233 -0
- data/lib/agentilda/linear/api.rb +14 -10
- data/lib/agentilda/linear/attribution.rb +5 -5
- data/lib/agentilda/linear/import.rb +30 -14
- data/lib/agentilda/linear/issue.rb +6 -3
- data/lib/agentilda/linear/mapping.rb +18 -17
- data/lib/agentilda/linear/push.rb +19 -12
- data/lib/agentilda/linear/survey.rb +3 -2
- data/lib/agentilda/linear/unit.rb +12 -12
- data/lib/agentilda/linear.rb +5 -6
- data/lib/agentilda/mailbox.rb +141 -0
- data/lib/agentilda/markdown.rb +1 -1
- data/lib/agentilda/progress_log.rb +5 -1
- data/lib/agentilda/publisher.rb +6 -3
- data/lib/agentilda/pull_request.rb +0 -186
- data/lib/agentilda/pull_requests.rb +230 -0
- data/lib/agentilda/reporter.rb +9 -3
- data/lib/agentilda/resync.rb +120 -52
- data/lib/agentilda/roster.rb +31 -48
- data/lib/agentilda/runner.rb +72 -389
- data/lib/agentilda/screen/ratatui/bar.rb +57 -0
- data/lib/agentilda/screen/ratatui/key_translator.rb +29 -0
- data/lib/agentilda/screen/ratatui.rb +318 -0
- data/lib/agentilda/state_file.rb +174 -0
- data/lib/agentilda/state_machine.rb +60 -32
- data/lib/agentilda/status.rb +120 -37
- data/lib/agentilda/subject.rb +133 -0
- data/lib/agentilda/tally.rb +17 -11
- data/lib/agentilda/transcript.rb +47 -16
- data/lib/agentilda/tree.rb +10 -6
- data/lib/agentilda/ui.rb +204 -190
- data/lib/agentilda/unblocker.rb +25 -11
- data/lib/agentilda/version.rb +1 -1
- data/lib/agentilda/worktree.rb +14 -18
- data/lib/agentilda.rb +57 -70
- metadata +65 -13
- data/Gemfile +0 -26
- data/Gemfile.lock +0 -261
- data/bin/create-plan-folder +0 -125
- data/lib/dry/cli/banner.rb +0 -293
data/lib/agentilda/resync.rb
CHANGED
|
@@ -15,15 +15,21 @@ module Agentilda
|
|
|
15
15
|
# folder whose status already holds is left alone, which is what stops
|
|
16
16
|
# ⭕️ Blocked and 🅱️ Product Blocked — deliberately identical invariants,
|
|
17
17
|
# distinguished only by the name — from collapsing into one.
|
|
18
|
-
# - **The
|
|
19
|
-
# before the padding rule
|
|
20
|
-
#
|
|
21
|
-
# `018.1` < `018.10` puts a single mixed-width folder in the
|
|
22
|
-
# the range. Both are renamed to `018.00
|
|
18
|
+
# - **The spelling is not canonical** — a folder written `018-⚪️--foo`
|
|
19
|
+
# before the `NNN.MM` padding rule or the ` → ` separator, or
|
|
20
|
+
# `18.1-⚪️--foo` by hand. Unpadded numbers read fine and sort wrong —
|
|
21
|
+
# `018.09` < `018.1` < `018.10` puts a single mixed-width folder in the
|
|
22
|
+
# middle of the range. Both are renamed, to `018.00-⚪️ → foo` and
|
|
23
|
+
# `018.01-⚪️ → foo`.
|
|
23
24
|
#
|
|
24
|
-
# The
|
|
25
|
-
#
|
|
26
|
-
#
|
|
25
|
+
# The repairs are applied in that order reversed, as **up to two renames
|
|
26
|
+
# per folder**: first the spelling, under the status the folder already
|
|
27
|
+
# claims — which is how a `--`-era folder migrates to the ` → ` form —
|
|
28
|
+
# and only then the status its contents justify. Both renames aim at
|
|
29
|
+
# {Feature.plan_dirname}, so they cannot disagree about the final name;
|
|
30
|
+
# splitting them keeps each auditable on its own, instead of one report
|
|
31
|
+
# line where a spelling migration also appears to have opinions about
|
|
32
|
+
# the state of the work.
|
|
27
33
|
class Dirs
|
|
28
34
|
# A proposed rename.
|
|
29
35
|
#
|
|
@@ -40,22 +46,41 @@ module Agentilda
|
|
|
40
46
|
# @!attribute [r] reason
|
|
41
47
|
# @return [String] why the current name is wrong
|
|
42
48
|
Change = Data.define(:dirname, :from, :to, :source, :target, :reason) do
|
|
43
|
-
# @return [String] a single auditable line
|
|
44
|
-
|
|
49
|
+
# @return [String] a single auditable line — `⇒` rather than `→`,
|
|
50
|
+
# which the folder names themselves now contain
|
|
51
|
+
def to_s = "#{dirname} ⇒ #{File.basename(target)} (#{reason})"
|
|
45
52
|
end
|
|
46
53
|
|
|
54
|
+
# The state a folder's contents justify, or the one it claims when
|
|
55
|
+
# nothing fits. What a rename moves toward, and what the dispatcher
|
|
56
|
+
# reads on a dry run, which renames nothing, so the preview still
|
|
57
|
+
# names the agents a real run would start.
|
|
58
|
+
#
|
|
59
|
+
# @param subject [Agentilda::Subject]
|
|
60
|
+
# @return [Agentilda::Status]
|
|
61
|
+
def self.target(subject) = subject.best_fit || subject.status
|
|
62
|
+
|
|
47
63
|
# @param tree [Agentilda::Tree]
|
|
48
|
-
|
|
64
|
+
# @param except [Array<String>] ordinals to leave alone however wrong
|
|
65
|
+
# their names are: the dispatcher passes the plans an agent is
|
|
66
|
+
# working in right now
|
|
67
|
+
def initialize(tree:, except: [])
|
|
49
68
|
@tree = tree
|
|
69
|
+
@except = except.map(&:to_s)
|
|
50
70
|
end
|
|
51
71
|
|
|
52
72
|
# @return [Agentilda::Tree]
|
|
53
73
|
attr_reader :tree
|
|
54
74
|
|
|
55
|
-
# What would change, without changing anything.
|
|
75
|
+
# What would change, without changing anything. A folder can appear
|
|
76
|
+
# twice: its spelling migration first, then its status correction.
|
|
56
77
|
#
|
|
57
78
|
# @return [Array<Agentilda::Resync::Dirs::Change>]
|
|
58
|
-
def plan
|
|
79
|
+
def plan
|
|
80
|
+
tree.subjects.flat_map do |subject|
|
|
81
|
+
@except.include?(subject.feature.ordinal.to_s) ? [] : changes_for(subject)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
59
84
|
|
|
60
85
|
# @param commit [Boolean] actually rename
|
|
61
86
|
# @return [Array<Agentilda::Resync::Dirs::Change>] what was proposed
|
|
@@ -71,48 +96,70 @@ module Agentilda
|
|
|
71
96
|
private
|
|
72
97
|
|
|
73
98
|
# A folder moves when the name it has is not the name it should have.
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
#
|
|
78
|
-
#
|
|
79
|
-
#
|
|
80
|
-
# Invariants are minimum requirements, so a ⚪️ folder that has grown a
|
|
81
|
-
# `plan.md` still satisfies ⚪️ and is ⭐️ anyway.
|
|
99
|
+
# {target} says what that name is; this says how to get there, as up
|
|
100
|
+
# to two renames applied in order. The status rename starts from where
|
|
101
|
+
# the spelling rename left the folder, not from where it stands now,
|
|
102
|
+
# so committing the steps sequentially never renames a path that has
|
|
103
|
+
# already moved out from under it.
|
|
82
104
|
#
|
|
83
105
|
# @param subject [Agentilda::Subject]
|
|
84
|
-
# @return [Agentilda::Resync::Dirs::Change
|
|
85
|
-
|
|
106
|
+
# @return [Array<Agentilda::Resync::Dirs::Change>] empty when the name
|
|
107
|
+
# is already right
|
|
108
|
+
def changes_for(subject)
|
|
86
109
|
feature = subject.feature
|
|
87
|
-
fit =
|
|
88
|
-
|
|
89
|
-
|
|
110
|
+
fit = self.class.target(subject)
|
|
111
|
+
steps = []
|
|
112
|
+
|
|
113
|
+
spelled = feature.dirname_as(subject.status)
|
|
114
|
+
if spelled != feature.dirname
|
|
115
|
+
steps << step(feature,
|
|
116
|
+
from: subject.status,
|
|
117
|
+
to: subject.status,
|
|
118
|
+
source: feature.path,
|
|
119
|
+
reason: spelling_reason(feature))
|
|
120
|
+
end
|
|
90
121
|
|
|
122
|
+
if fit.key != subject.status.key
|
|
123
|
+
source = steps.empty? ? feature.path : steps.last.target
|
|
124
|
+
steps << step(feature,
|
|
125
|
+
from: subject.status,
|
|
126
|
+
to: fit,
|
|
127
|
+
source:,
|
|
128
|
+
reason: subject.violation || "contents now justify #{fit.label}")
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
steps
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# @param feature [Agentilda::Feature]
|
|
135
|
+
# @param from [Agentilda::Status]
|
|
136
|
+
# @param to [Agentilda::Status]
|
|
137
|
+
# @param source [String] absolute path the rename starts from
|
|
138
|
+
# @param reason [String]
|
|
139
|
+
# @return [Agentilda::Resync::Dirs::Change]
|
|
140
|
+
def step(feature, from:, to:, source:, reason:)
|
|
91
141
|
Change.new(
|
|
92
|
-
dirname:
|
|
93
|
-
from:
|
|
94
|
-
to:
|
|
95
|
-
source
|
|
96
|
-
target:
|
|
97
|
-
reason:
|
|
142
|
+
dirname: File.basename(source),
|
|
143
|
+
from: from.key,
|
|
144
|
+
to: to.key,
|
|
145
|
+
source:,
|
|
146
|
+
target: File.join(File.dirname(feature.path), feature.dirname_as(to)),
|
|
147
|
+
reason:
|
|
98
148
|
)
|
|
99
149
|
end
|
|
100
150
|
|
|
101
|
-
# @param
|
|
102
|
-
# @
|
|
103
|
-
|
|
104
|
-
def reason_for(subject, fit)
|
|
105
|
-
feature = subject.feature
|
|
106
|
-
return subject.violation || "contents now justify #{fit.label}" unless fit.key == subject.status.key
|
|
151
|
+
# @param feature [Agentilda::Feature]
|
|
152
|
+
# @return [String] why the spelling is wrong, status aside
|
|
153
|
+
def spelling_reason(feature)
|
|
107
154
|
return "#{feature.dirname_ordinal} is not padded to #{feature.ordinal}" unless feature.padded?
|
|
108
155
|
|
|
109
|
-
"name is not in canonical NNN.MM-<emoji
|
|
156
|
+
"name is not in canonical NNN.MM-<emoji> → <slug> form"
|
|
110
157
|
end
|
|
111
158
|
|
|
112
159
|
# A rename that finds its target occupied has not happened, and saying
|
|
113
160
|
# nothing about it would leave the folder misnamed with a report
|
|
114
161
|
# claiming otherwise. Two folders can want the same canonical name —
|
|
115
|
-
# `018-⚪️--foo` and `018.00
|
|
162
|
+
# `018-⚪️--foo` and `018.00-⚪️ → foo` are the same plan written twice.
|
|
116
163
|
#
|
|
117
164
|
# @param change [Agentilda::Resync::Dirs::Change]
|
|
118
165
|
# @return [void]
|
|
@@ -236,10 +283,14 @@ module Agentilda
|
|
|
236
283
|
# @param pull [Hash]
|
|
237
284
|
# @return [Agentilda::Resync::Prs::Change]
|
|
238
285
|
def restamped(pull)
|
|
239
|
-
Change.new(number: pull[:number],
|
|
286
|
+
Change.new(number: pull[:number],
|
|
287
|
+
title: pull[:title],
|
|
288
|
+
ordinal: nil,
|
|
289
|
+
ambiguous: false,
|
|
240
290
|
new_title: pull[:title].sub(STALE, "[#{Agentilda::NO_PLAN_PREFIX}] "),
|
|
241
291
|
reason: "the no-plan marker is now [#{Agentilda::NO_PLAN_PREFIX}]",
|
|
242
|
-
assumed: false,
|
|
292
|
+
assumed: false,
|
|
293
|
+
adopted: false)
|
|
243
294
|
end
|
|
244
295
|
|
|
245
296
|
# @param pulls [Array<Hash>]
|
|
@@ -261,7 +312,7 @@ module Agentilda
|
|
|
261
312
|
adoptees = create ? adoption.call(pulls) : adoption.plan(pulls)
|
|
262
313
|
by_number = adoptees.to_h { |a| [a.pull[:number], a] }
|
|
263
314
|
|
|
264
|
-
changes.map { |c|
|
|
315
|
+
changes.map { |c| c.ambiguous? && by_number[c.number] ? adopted(c, by_number[c.number]) : c }
|
|
265
316
|
end
|
|
266
317
|
|
|
267
318
|
# @return [Hash{Integer => Hash}]
|
|
@@ -272,11 +323,11 @@ module Agentilda
|
|
|
272
323
|
# @return [Agentilda::Resync::Prs::Change]
|
|
273
324
|
def adopted(change, adoptee)
|
|
274
325
|
change.with(
|
|
275
|
-
ordinal:
|
|
326
|
+
ordinal: adoptee.ordinal,
|
|
276
327
|
new_title: "#{adoptee.ordinal.to_prefix} #{change.title}",
|
|
277
|
-
reason:
|
|
328
|
+
reason: "#{change.reason} — adopted into #{adoptee.dirname}",
|
|
278
329
|
ambiguous: false,
|
|
279
|
-
adopted:
|
|
330
|
+
adopted: true
|
|
280
331
|
)
|
|
281
332
|
end
|
|
282
333
|
|
|
@@ -331,8 +382,14 @@ module Agentilda
|
|
|
331
382
|
# @param why [String]
|
|
332
383
|
# @return [Agentilda::Resync::Prs::Change]
|
|
333
384
|
def resolved(pull, ordinal, why)
|
|
334
|
-
Change.new(number: pull[:number],
|
|
335
|
-
|
|
385
|
+
Change.new(number: pull[:number],
|
|
386
|
+
title: pull[:title],
|
|
387
|
+
ordinal:,
|
|
388
|
+
reason: why,
|
|
389
|
+
new_title: "#{ordinal.to_prefix} #{pull[:title]}",
|
|
390
|
+
ambiguous: false,
|
|
391
|
+
assumed: false,
|
|
392
|
+
adopted: false)
|
|
336
393
|
end
|
|
337
394
|
|
|
338
395
|
# Nothing resolved, so this is developer work — but that is an assertion
|
|
@@ -341,17 +398,28 @@ module Agentilda
|
|
|
341
398
|
# @param pull [Hash]
|
|
342
399
|
# @return [Agentilda::Resync::Prs::Change]
|
|
343
400
|
def no_plan(pull)
|
|
344
|
-
Change.new(number: pull[:number],
|
|
401
|
+
Change.new(number: pull[:number],
|
|
402
|
+
title: pull[:title],
|
|
403
|
+
ordinal: nil,
|
|
345
404
|
new_title: "[#{Agentilda::NO_PLAN_PREFIX}] #{pull[:title]}",
|
|
346
|
-
reason: "no plan resolved from the branch or the diff",
|
|
405
|
+
reason: "no plan resolved from the branch or the diff",
|
|
406
|
+
ambiguous: false,
|
|
407
|
+
assumed: true,
|
|
408
|
+
adopted: false)
|
|
347
409
|
end
|
|
348
410
|
|
|
349
411
|
# @param pull [Hash]
|
|
350
412
|
# @param why [String]
|
|
351
413
|
# @return [Agentilda::Resync::Prs::Change]
|
|
352
414
|
def flag(pull, why)
|
|
353
|
-
Change.new(number: pull[:number],
|
|
354
|
-
|
|
415
|
+
Change.new(number: pull[:number],
|
|
416
|
+
title: pull[:title],
|
|
417
|
+
new_title: nil,
|
|
418
|
+
ordinal: nil,
|
|
419
|
+
reason: why,
|
|
420
|
+
ambiguous: true,
|
|
421
|
+
assumed: false,
|
|
422
|
+
adopted: false)
|
|
355
423
|
end
|
|
356
424
|
end
|
|
357
425
|
end
|
data/lib/agentilda/roster.rb
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "agentilda/status"
|
|
4
|
+
require "agentilda/state_machine"
|
|
5
|
+
|
|
3
6
|
module Agentilda
|
|
4
7
|
# `agentilda agents` — who the specialists are and what each is offered
|
|
5
8
|
# work from. Reading one in full is {Viewer}'s job: the definition file is
|
|
@@ -21,6 +24,11 @@ module Agentilda
|
|
|
21
24
|
# `run` never offers it work.
|
|
22
25
|
READ_ONLY = "read-only"
|
|
23
26
|
|
|
27
|
+
# What an agent that handles only {StateMachine::SETTLED} states advances
|
|
28
|
+
# to. `run` never starts it; a command does, and `resync` then names the
|
|
29
|
+
# folder by what its files justify.
|
|
30
|
+
BY_CONTENTS = "by contents"
|
|
31
|
+
|
|
24
32
|
# @param agents [Agentilda::Agents]
|
|
25
33
|
def initialize(agents: Agents.new)
|
|
26
34
|
@agents = agents
|
|
@@ -29,26 +37,33 @@ module Agentilda
|
|
|
29
37
|
# @return [Agentilda::Agents]
|
|
30
38
|
attr_reader :agents
|
|
31
39
|
|
|
32
|
-
# One
|
|
40
|
+
# One row per agent, in name order, framed by {UI.table}.
|
|
33
41
|
#
|
|
34
42
|
# @return [String] newline-terminated
|
|
35
43
|
def list
|
|
36
44
|
all = agents.all
|
|
37
45
|
return "No agent definitions in #{agents.dir}\n" if all.empty?
|
|
38
46
|
|
|
39
|
-
|
|
47
|
+
UI.table(all.map { |agent| row(agent) }, header: HEADINGS)
|
|
40
48
|
end
|
|
41
49
|
|
|
42
50
|
private
|
|
43
51
|
|
|
44
|
-
# A state as
|
|
45
|
-
#
|
|
52
|
+
# A state as its words alone, deliberately without the emoji that every
|
|
53
|
+
# other rendering of a state carries. The table widget pads a cell to a
|
|
54
|
+
# width it measures in characters, while the terminal draws a status emoji
|
|
55
|
+
# in two cells; the variation-selector ones (⭕️ 🅱️ ⚪️ ⭐️ 🕰️) are two
|
|
56
|
+
# characters drawn in two cells, so no single count is right for all of
|
|
57
|
+
# them. A row carrying one lands short or long and the frame stops lining
|
|
58
|
+
# up. The label says the same thing unambiguously, and is worth more than
|
|
59
|
+
# the emoji is. {Reporter} lays its own columns out with {UI.fit} and so
|
|
60
|
+
# keeps the emoji.
|
|
46
61
|
#
|
|
47
62
|
# @param key [Symbol]
|
|
48
63
|
# @return [String]
|
|
49
64
|
def state(key)
|
|
50
65
|
status = STATUS_BY_KEY[key] or return key.to_s
|
|
51
|
-
|
|
66
|
+
status.label
|
|
52
67
|
end
|
|
53
68
|
|
|
54
69
|
# @param agent [Agentilda::Agent]
|
|
@@ -57,54 +72,22 @@ module Agentilda
|
|
|
57
72
|
|
|
58
73
|
# @param agent [Agentilda::Agent]
|
|
59
74
|
# @return [String]
|
|
60
|
-
def advances(agent)
|
|
75
|
+
def advances(agent)
|
|
76
|
+
return state(agent.advances_to) if agent.advances_to
|
|
77
|
+
return BY_CONTENTS if agent.handles.any? && agent.handles.all? { |key| StateMachine::SETTLED.include?(key) }
|
|
61
78
|
|
|
62
|
-
|
|
63
|
-
# @return [Integer]
|
|
64
|
-
def name_width(all) = @name_width ||= all.map { |a| UI.display_width(a.name) }.max.to_i + 2
|
|
65
|
-
|
|
66
|
-
# @param all [Array<Agentilda::Agent>]
|
|
67
|
-
# @return [Integer]
|
|
68
|
-
def handles_width(all)
|
|
69
|
-
@handles_width ||= (all.map { |a| UI.display_width(handles(a)) } + [UI.display_width(HEADINGS[1])]).max.to_i + 2
|
|
79
|
+
READ_ONLY
|
|
70
80
|
end
|
|
71
81
|
|
|
72
|
-
#
|
|
73
|
-
# @return [Integer]
|
|
74
|
-
def advances_width(all)
|
|
75
|
-
@advances_width ||= (all.map { |a| UI.display_width(advances(a)) } + [UI.display_width(HEADINGS[2])]).max.to_i + 2
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
# Cells arrive fitted and then painted, in that order: escape codes count
|
|
79
|
-
# toward a string's length, so padding a coloured string pads it to a width
|
|
80
|
-
# of which several characters are invisible. See {Reporter#row}.
|
|
82
|
+
# The widget measures and pads the cells, so they arrive painted only.
|
|
81
83
|
#
|
|
82
|
-
# @return [String]
|
|
83
|
-
def line(name, handled, advances, model)
|
|
84
|
-
format(" %s %s %s %s", name, handled, advances, model).rstrip
|
|
85
|
-
end
|
|
86
|
-
|
|
87
|
-
# @param all [Array<Agentilda::Agent>]
|
|
88
|
-
# @return [String]
|
|
89
|
-
def header(all)
|
|
90
|
-
text = line(UI.fit(HEADINGS[0], name_width(all)),
|
|
91
|
-
UI.fit(HEADINGS[1], handles_width(all)),
|
|
92
|
-
UI.fit(HEADINGS[2], advances_width(all)),
|
|
93
|
-
HEADINGS[3])
|
|
94
|
-
|
|
95
|
-
rule = " " + ("─" * (UI.display_width(text) - 2))
|
|
96
|
-
|
|
97
|
-
[UI.paint(text, :bold), UI.paint(rule, :bright_yellow)].join("\n")
|
|
98
|
-
end
|
|
99
|
-
|
|
100
84
|
# @param agent [Agentilda::Agent]
|
|
101
|
-
# @
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
UI.paint(agent.model.to_s, :bright_black))
|
|
85
|
+
# @return [Array<String>] one cell per entry in {HEADINGS}
|
|
86
|
+
def row(agent)
|
|
87
|
+
[UI.paint(agent.name, :bright_cyan),
|
|
88
|
+
handles(agent),
|
|
89
|
+
UI.paint(advances(agent), agent.read_only? ? :bright_black : :green),
|
|
90
|
+
UI.paint(agent.model.to_s, :bright_black)]
|
|
108
91
|
end
|
|
109
92
|
end
|
|
110
93
|
end
|