agent-horde 0.1.6 → 0.2.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.
@@ -7,13 +7,14 @@ import type { FactEntry } from "../ledger/facts.js";
7
7
  import * as path from "node:path";
8
8
  import { CLUSTER_THRESHOLD } from "../ledger/clusters.js";
9
9
  import { collaborationState } from "../ledger/collaboration.js";
10
- import { addFinding, amendFinding, answerCitations, findingReviewState, findingRevision, findingSummaries, listFindingsForRun, pendingReviewsForMember, staleAnswerCitations, verifyFinding } from "../ledger/findings.js";
10
+ import { addFinding, amendFinding, answerCitations, findingReviewState, findingRevision, findingSummaries, listFindingsForRun, pendingReviewsForMember, staleAnswerFor, verifyFinding } from "../ledger/findings.js";
11
11
  import { textOverlap } from "../ledger/text-match.js";
12
12
  import { addIdea, claimIdea, dropIdea, finishIdea, listIdeas, ruleOutIdea } from "../ledger/ideas.js";
13
13
  import type { Idea } from "../ledger/ideas.js";
14
14
  import { claimResource, listResources } from "../ledger/resources.js";
15
15
  import { cleanStopRequestedFor, confirmSolved, contestSolved, markSolved, readSolved } from "../ledger/run-signals.js";
16
16
  import { oneLine } from "../format.js";
17
+ import type { SiblingHorde } from "../member.js";
17
18
  import { validateFindingResourceCitations } from "../citations.js";
18
19
  import { idleHint } from "./hints.js";
19
20
  import type { HordeToolParams } from "./params.js";
@@ -54,6 +55,29 @@ interface ActionContext {
54
55
  cleanStop: boolean;
55
56
  /** The member's role instruction, when one was assigned. */
56
57
  role: string | undefined;
58
+ /** Other hordes of the swarm whose ledgers the read actions include, tagged by label. */
59
+ siblings: SiblingHorde[];
60
+ }
61
+
62
+ /**
63
+ * Shared read, local write: the read actions append each sibling horde's entries, tagged `[hN]`,
64
+ * so a fact, a rule-out or a resource another horde recorded is not re-derived here. A sibling
65
+ * whose ledger cannot be read is reported, not fatal.
66
+ */
67
+ function siblingSections(siblings: SiblingHorde[], what: string, lines: (hordeDir: string) => string[]): string {
68
+ return siblings
69
+ .map((sibling) => {
70
+ const tag = `[${sibling.label}]`;
71
+ let entries: string[];
72
+ try {
73
+ entries = lines(sibling.hordeDir).map((line) => `${tag} ${line}`);
74
+ } catch (error) {
75
+ entries = [`${tag} unreadable: ${oneLine(error instanceof Error ? error.message : String(error), 120)}`];
76
+ }
77
+ const head = `${tag} ${what} of horde ${sibling.label} (read-only; ${sibling.hordeDir}):`;
78
+ return `\n\n${head}\n${entries.length > 0 ? entries.join("\n") : `${tag} none`}`;
79
+ })
80
+ .join("");
57
81
  }
58
82
 
59
83
  export function isVerifierRole(role: string | undefined): boolean {
@@ -108,18 +132,19 @@ function closedIdea(ctx: ActionContext, idea: Idea, text: string): ActionResult
108
132
  return { text: `${text}${hint}`, id: idea.id };
109
133
  }
110
134
 
111
- function listAction({ hordeDir }: ActionContext): ActionResult {
135
+ function ideaLine(idea: Idea): string {
136
+ const owner = idea.owner ? ` @${idea.owner}` : "";
137
+ const note = idea.note ? `: ${idea.note}` : "";
138
+ return `#${idea.id} [${idea.mark}]${owner} ${idea.text}${note}`;
139
+ }
140
+
141
+ function listAction({ hordeDir, siblings }: ActionContext): ActionResult {
112
142
  const ideas = listIdeas(hordeDir);
113
- if (ideas.length === 0) return { text: "IDEAS.md has no ideas yet" };
114
- return {
115
- text: ideas
116
- .map((idea) => {
117
- const owner = idea.owner ? ` @${idea.owner}` : "";
118
- const note = idea.note ? `: ${idea.note}` : "";
119
- return `#${idea.id} [${idea.mark}]${owner} ${idea.text}${note}`;
120
- })
121
- .join("\n"),
122
- };
143
+ const own = ideas.length === 0 ? "IDEAS.md has no ideas yet" : ideas.map(ideaLine).join("\n");
144
+ // Only closed ideas cross hordes: results prevent re-derivation, open ones would invite claims.
145
+ const shared = siblingSections(siblings, "done and ruled-out ideas", (dir) =>
146
+ listIdeas(dir).filter((idea) => idea.mark === "x" || idea.mark === "!").map(ideaLine));
147
+ return { text: `${own}${shared}` };
123
148
  }
124
149
 
125
150
  async function postAction({ hordeDir, member, params }: ActionContext): Promise<ActionResult> {
@@ -282,14 +307,18 @@ async function factAction({ hordeDir, member, params }: ActionContext): Promise<
282
307
  return { text: `Recorded fact T${fact.id}: ${fact.text}${provisional}`, id: fact.id };
283
308
  }
284
309
 
285
- function factsAction({ hordeDir }: ActionContext): ActionResult {
286
- const facts = listFacts(hordeDir);
287
- if (facts.length === 0) return { text: "FACTS.md has no facts yet" };
310
+ function factLines(hordeDir: string): string[] {
288
311
  const status = (fact: FactEntry): string => {
289
312
  if (!fact.provisionalOn) return "";
290
313
  return factUnsettled(hordeDir, fact) ? ` [provisional: ${fact.provisionalOn} open]` : ` [was provisional on ${fact.provisionalOn}, now closed]`;
291
314
  };
292
- return { text: facts.map((fact) => `T${fact.id} ${fact.text} @${fact.author}${status(fact)}`).join("\n") };
315
+ return listFacts(hordeDir).map((fact) => `T${fact.id} ${fact.text} @${fact.author}${status(fact)}`);
316
+ }
317
+
318
+ function factsAction({ hordeDir, siblings }: ActionContext): ActionResult {
319
+ const own = factLines(hordeDir);
320
+ const shared = siblingSections(siblings, "facts", factLines);
321
+ return { text: `${own.length === 0 ? "FACTS.md has no facts yet" : own.join("\n")}${shared}` };
293
322
  }
294
323
 
295
324
  /** An answer that quotes a provisional fact as settled asserts what an open idea is still auditing. */
@@ -421,29 +450,29 @@ async function resourceAction(ctx: ActionContext): Promise<ActionResult> {
421
450
  };
422
451
  }
423
452
 
424
- function resourcesAction({ hordeDir }: ActionContext): ActionResult {
425
- const resources = listResources(hordeDir);
426
- if (resources.length === 0) return { text: "RESOURCES.md has no resources yet" };
427
- return {
428
- text: resources
429
- .map(
430
- (resource) =>
431
- `R${resource.id} ${resource.name} @${resource.owner}` +
432
- `${resource.note ? `: ${resource.note}` : ""}` +
433
- `${resource.reusers.length > 0 ? ` reuse=${resource.reusers.join(",")}` : ""}`,
434
- )
435
- .join("\n"),
436
- };
453
+ function resourceLines(hordeDir: string): string[] {
454
+ return listResources(hordeDir).map(
455
+ (resource) =>
456
+ `R${resource.id} ${resource.name} @${resource.owner}` +
457
+ `${resource.note ? `: ${resource.note}` : ""}` +
458
+ `${resource.reusers.length > 0 ? ` reuse=${resource.reusers.join(",")}` : ""}`,
459
+ );
460
+ }
461
+
462
+ function resourcesAction({ hordeDir, siblings }: ActionContext): ActionResult {
463
+ const own = resourceLines(hordeDir);
464
+ const shared = siblingSections(siblings, "resources (reuse by path; cite the path, not the R#)", resourceLines);
465
+ return { text: `${own.length === 0 ? "RESOURCES.md has no resources yet" : own.join("\n")}${shared}` };
437
466
  }
438
467
 
439
468
  /**
440
- * An answer that cites a superseded finding, or one amended since the proposal pinned it, ships a
441
- * stale reading as the run's result. The proposer cites the current text instead; a validator is
442
- * refused until the proposer re-proposes.
469
+ * An answer that cites a superseded finding, or a finding amended or an artifact republished since
470
+ * the proposal pinned it, ships a stale reading as the run's result. The proposer cites the
471
+ * current text instead; a validator is refused until the proposer re-proposes.
443
472
  */
444
473
  function staleAnswer(ctx: ActionContext, text: string, cites?: Record<string, string>): string | undefined {
445
474
  const runId = requireRun(ctx);
446
- const stale = staleAnswerCitations(listFindingsForRun(ctx.hordeDir, runId), text, cites);
475
+ const stale = staleAnswerFor(ctx.hordeDir, runId, text, cites);
447
476
  if (!stale.length) return undefined;
448
477
  return `cites ${stale.join(", ")}`;
449
478
  }
@@ -460,7 +489,7 @@ async function solvedAction(ctx: ActionContext): Promise<ActionResult> {
460
489
  runId,
461
490
  ctx.member,
462
491
  ctx.params.text,
463
- answerCitations(listFindingsForRun(ctx.hordeDir, runId), text),
492
+ answerCitations(listFindingsForRun(ctx.hordeDir, runId), text, collaborationState(ctx.hordeDir, runId).artifacts),
464
493
  );
465
494
  return {
466
495
  text: alreadySolved
@@ -564,11 +593,11 @@ export async function runHordeAction(
564
593
  member: string,
565
594
  params: HordeToolParams,
566
595
  runId: string | undefined = process.env.PI_HORDE_RUN,
567
- tracking: { attempt?: string; requestId?: string; role?: string | undefined } = {},
596
+ tracking: { attempt?: string; requestId?: string; role?: string | undefined; siblings?: SiblingHorde[] | undefined } = {},
568
597
  ): Promise<ActionResult> {
569
598
  const cleanStop = Boolean(runId && cleanStopRequestedFor(hordeDir, runId, member));
570
- const role = tracking.role;
571
- const ctx: EventContext = { hordeDir, runId: runId ?? "", actor: member, ...tracking, requestId: params.requestId ?? tracking.requestId };
599
+ const { role, siblings = [], ...eventTracking } = tracking;
600
+ const ctx: EventContext = { hordeDir, runId: runId ?? "", actor: member, ...eventTracking, requestId: params.requestId ?? tracking.requestId };
572
601
  if (COLLABORATION_ACTIONS.has(params.action)) {
573
602
  if (!runId) throw new Error("horde: collaboration needs an active run");
574
603
  return runCollaborationAction(ctx, params);
@@ -580,7 +609,7 @@ export async function runHordeAction(
580
609
  const intent = audit ? await recordEvent({ ...ctx, requestId: ctx.requestId ? `${ctx.requestId}:intent` : undefined }, params.action, { outcome: "intent", payload: { input: params } }) : undefined;
581
610
  try {
582
611
  const refused = cleanStop && NEW_WORK_ACTIONS.has(params.action);
583
- const result = refused ? { text: CLEAN_STOP_NOTICE } : await handler({ hordeDir, member, params, runId, cleanStop, role });
612
+ const result = refused ? { text: CLEAN_STOP_NOTICE } : await handler({ hordeDir, member, params, runId, cleanStop, role, siblings });
584
613
  // A refusal and a no-op both record `noop`; the no-op keeps `result.noop` so the graph and the
585
614
  // report can tell an absorbed duplicate from refused work.
586
615
  if (audit) await recordEvent(ctx, params.action, { outcome: refused || result.noop ? "noop" : "applied", payload: { input: params, result }, parents: intent ? [intent.eventId] : [] });
@@ -107,11 +107,12 @@ function responseSubject(c: CollaborationCommand, m: PeerMessage): string {
107
107
  }
108
108
 
109
109
  /**
110
- * A blocking request closes on the recipient's response plus the sender's own supporting check of
111
- * the current revision. Before any response the sender may still withdraw its own challenge on an
112
- * F# or A#: a challenger that verified the claim itself, or watched others verify it, records a
113
- * supporting review of the current revision and cites evidence on it. Limits that review
114
- * discloses stay acceptance debt on the review record; the withdrawal clears only the dispute.
110
+ * A blocking request closes on the recipient's response plus a supporting check of the current
111
+ * revision: the sender's own review on a challenge, or the recipient's review when the sender owns
112
+ * the target and so may not review it. Before any response the sender may still withdraw its own
113
+ * challenge on an F# or A#: a challenger that verified the claim itself, or watched others verify
114
+ * it, records a supporting review of the current revision and cites evidence on it. Limits that
115
+ * review discloses stay acceptance debt on the review record; the withdrawal clears only the dispute.
115
116
  */
116
117
  function validateResolution(c: CollaborationCommand, m: PeerMessage, refs: string[]): void {
117
118
  if (m.sender === m.to) throw new Error("horde: a blocking request needs an independent resolver");
@@ -121,13 +122,19 @@ function validateResolution(c: CollaborationCommand, m: PeerMessage, refs: strin
121
122
  if (refs.length === 0 || (withdrawal && !reviewable) || (!withdrawal && !["responded", "deferred"].includes(m.state))) {
122
123
  throw new Error("horde: blocking resolution needs a response and evidence");
123
124
  }
124
- const revision = targetInfo(c.ctx, c.state, target).revision;
125
+ const { owner, revision } = targetInfo(c.ctx, c.state, target);
125
126
  if ((c.params.revision ?? m.responseRevision ?? revision) !== revision) throw new Error(`horde: response is stale; explicitly review and resolve ${target}@${revision}, or ask the owner to respond again`);
126
127
  if (!refs.some((id) => c.state.evidence.some((e) => e.id === id && e.target === target && e.revision === revision))) {
127
128
  const detail = staleEvidence(c.state, refs, target, revision) ?? `none of ${refs.join(", ")} targets ${target}@${revision.slice(0, 12)}`;
128
129
  throw new Error(`horde: resolution evidence must address the current response target revision: ${detail}; attach evidence to that revision first`);
129
130
  }
130
131
  if (!reviewable) return;
132
+ if (owner === c.ctx.actor) requireRecipientReview(c, m, target, revision, withdrawal);
133
+ else requireOwnReview(c, target, revision, withdrawal);
134
+ }
135
+
136
+ /** A challenger settles its own dispute: its latest review of the current revision must support. */
137
+ function requireOwnReview(c: CollaborationCommand, target: string, revision: string, withdrawal: boolean): void {
131
138
  const review = c.state.reviews.findLast((r) => r.target === target && r.revision === revision && r.actor === c.ctx.actor);
132
139
  if (!review || review.verdict !== "supports") {
133
140
  throw new Error(withdrawal
@@ -137,6 +144,24 @@ function validateResolution(c: CollaborationCommand, m: PeerMessage, refs: strin
137
144
  if (!withdrawal && target.startsWith("A") && review.unchecked) throw new Error("horde: resolve requires your supporting review of the current revision; finding limitations remain acceptance debt");
138
145
  }
139
146
 
147
+ /**
148
+ * The owner of the target cannot review it, so a request it sent about its own work (a re-review
149
+ * after a correction) resolves on the recipient's supporting review of the current revision. The
150
+ * refusal names what is missing, since telling the owner to review its own artifact leads nowhere.
151
+ */
152
+ function requireRecipientReview(c: CollaborationCommand, m: PeerMessage, target: string, revision: string, withdrawal: boolean): void {
153
+ const review = c.state.reviews.findLast((r) => r.target === target && r.revision === revision && r.actor === m.to);
154
+ const short = `${target}@${revision.slice(0, 12)}`;
155
+ const prefix = `horde: ${target} is yours, so ${m.id} resolves on ${m.to}'s review`;
156
+ if (!review || review.verdict !== "supports") {
157
+ const missing = review ? `${review.id} is ${review.verdict}, not supports` : `no supporting review of ${short} by ${m.to} yet`;
158
+ throw new Error(`${prefix}: ${missing}; ask ${m.to} to review the current revision`);
159
+ }
160
+ if (!withdrawal && target.startsWith("A") && review.unchecked) {
161
+ throw new Error(`${prefix}: ${review.id} left unchecked: ${review.unchecked}; ask ${m.to} for a full review of ${short}`);
162
+ }
163
+ }
164
+
140
165
  function attachEvidence(c: CollaborationCommand): EventDraft {
141
166
  const { ctx, state, params: p, seq } = c;
142
167
  const target = requireParam(p.target, "target", p.action);
@@ -2,6 +2,7 @@ import * as path from "node:path";
2
2
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
3
3
  import { cleanStopRequestedFor, writeHeartbeat } from "../ledger/run-signals.js";
4
4
  import { envNumber } from "../format.js";
5
+ import { parseSiblings } from "../member.js";
5
6
  import { ReminderGate, goalReminder } from "../reminder.js";
6
7
  import { isVerifierRole, runHordeAction } from "./actions.js";
7
8
  import { pendingVerificationHint, solvedHint } from "./hints.js";
@@ -31,6 +32,7 @@ export function registerMemberTool(pi: ExtensionAPI): void {
31
32
  throw new Error("agent-horde: PI_HORDE_DIR and PI_HORDE_NAME must be set for a horde member");
32
33
  }
33
34
  const attempt = process.env.PI_HORDE_ATTEMPT ?? `pid-${process.pid}`;
35
+ const siblings = parseSiblings(process.env.PI_HORDE_SIBLINGS);
34
36
  const activeRun = process.env.PI_HORDE_RUN;
35
37
  if (activeRun) registerInbox(pi, { hordeDir, runId: activeRun, actor: member, attempt });
36
38
  // One member per process, so one gate is the per-member reminder counter.
@@ -64,7 +66,7 @@ export function registerMemberTool(pi: ExtensionAPI): void {
64
66
  async execute(toolCallId, params) {
65
67
  const runId = process.env.PI_HORDE_RUN;
66
68
  const role = process.env.PI_HORDE_ROLE;
67
- const result = await runHordeAction(hordeDir, member, params, runId, { attempt, requestId: toolCallId, role });
69
+ const result = await runHordeAction(hordeDir, member, params, runId, { attempt, requestId: toolCallId, role, siblings });
68
70
  const runDir = runId ? path.join(hordeDir, "runs", runId) : undefined;
69
71
  if (runDir && MUTATING_ACTIONS.has(params.action)) touchHeartbeat(runDir, member);
70
72
  let text = compactReceipt(result.text, params) + solvedHint(hordeDir, runId, member, params.action);
package/src/widget.ts CHANGED
@@ -1,17 +1,30 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { truncateToWidth } from "@earendil-works/pi-tui";
3
- import { formatElapsed } from "./format.js";
3
+ import { formatElapsed, formatTokens } from "./format.js";
4
4
  import type { MemberState } from "./member.js";
5
- import { memberActivity, statusCounts } from "./prompt.js";
5
+ import { memberActivity, statusCounts, totalUsage } from "./prompt.js";
6
6
 
7
7
  interface WidgetRun {
8
8
  id: string;
9
+ horde: number;
9
10
  members: MemberState[];
10
11
  startedAt: number;
11
12
  cleanStopUntil?: number | undefined;
12
13
  dashboardUrl?: string | undefined;
13
14
  /** Steps still running after members finished, e.g. ["deliverable", "post-mortem"]. */
14
15
  finalizing?: string[] | undefined;
16
+ reported?: boolean | undefined;
17
+ finalized?: boolean | undefined;
18
+ outcome?: string | undefined;
19
+ }
20
+
21
+ interface WidgetSwarm {
22
+ id: string;
23
+ runs: WidgetRun[];
24
+ startedAt: number;
25
+ dashboardUrl?: string | undefined;
26
+ /** The swarm step still running after every horde finalized, e.g. ["merge"]. */
27
+ finalizing?: string[] | undefined;
15
28
  }
16
29
 
17
30
  const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
@@ -65,8 +78,8 @@ function memberLine(m: MemberState, now: number, frame: number, theme: Theme): s
65
78
  );
66
79
  }
67
80
 
68
- export function widgetLines(run: WidgetRun, now: number, width: number, theme: Theme): string[] {
69
- const frame = Math.floor(now / 100) % SPINNER.length;
81
+ /** The widget for one horde: a header, then a line per member (a glyph strip past eight). */
82
+ function runLines(run: WidgetRun, now: number, frame: number, theme: Theme): string[] {
70
83
  const lines = [headerLine(run, now, theme)];
71
84
  if (run.members.length > COLLAPSE_ABOVE) {
72
85
  lines.push(` ${run.members.map((m) => statusGlyph(m, frame, theme)).join(" ")}`);
@@ -77,22 +90,81 @@ export function widgetLines(run: WidgetRun, now: number, width: number, theme: T
77
90
  } else {
78
91
  for (const m of run.members) lines.push(memberLine(m, now, frame, theme));
79
92
  }
93
+ return lines;
94
+ }
95
+
96
+ /** What one horde of a swarm is doing, in a word. */
97
+ function hordeActivity(run: WidgetRun, c: ReturnType<typeof statusCounts>): string {
98
+ if (run.finalized) return run.outcome ?? "finished";
99
+ if (run.finalizing && run.finalizing.length > 0) return `synthesizing ${run.finalizing.join(", ")}`;
100
+ if (run.reported) return "finalizing";
101
+ if (run.cleanStopUntil !== undefined) return "stopping";
102
+ return c.running + c.starting > 0 ? "running" : "finishing";
103
+ }
104
+
105
+ /** One line per horde: `h2 11/12 running 1 done t120 45.2k tok $1.20 running`. */
106
+ export function hordeLine(run: WidgetRun, now: number, frame: number, theme: Theme): string {
107
+ const c = statusCounts(run.members);
108
+ const usage = totalUsage(run.members);
109
+ const live = run.members.find((m) => m.status === "running");
110
+ const glyph = run.finalized
111
+ ? theme.fg("success", "✓")
112
+ : live
113
+ ? theme.fg("accent", SPINNER[frame % SPINNER.length] ?? "⠋")
114
+ : theme.fg("dim", "·");
115
+ const parts = [
116
+ `${c.running + c.starting}/${run.members.length} running`,
117
+ `${c.done} done`,
118
+ ];
119
+ if (c.failed) parts.push(theme.fg("error", `${c.failed} failed`));
120
+ if (c.stopped) parts.push(theme.fg("warning", `${c.stopped} stopped`));
121
+ parts.push(theme.fg("dim", `t${usage.turns} ${formatTokens(usage.input + usage.output)} tok $${usage.cost.toFixed(2)}`));
122
+ parts.push(theme.fg("muted", hordeActivity(run, c)));
123
+ return ` ${glyph} ${`h${run.horde}`.padEnd(3)} ${parts.join(" ")}`;
124
+ }
125
+
126
+ function swarmHeader(swarm: WidgetSwarm, now: number, theme: Theme): string {
127
+ const members = swarm.runs.flatMap((run) => run.members);
128
+ const c = statusCounts(members);
129
+ const parts = [
130
+ theme.bold(theme.fg("accent", `swarm ${swarm.id}`)),
131
+ `${swarm.runs.length} hordes`,
132
+ `${c.running + c.starting} running`,
133
+ `${c.done} done`,
134
+ ];
135
+ if (c.failed) parts.push(theme.fg("error", `${c.failed} failed`));
136
+ if (c.stopped) parts.push(theme.fg("warning", `${c.stopped} stopped`));
137
+ parts.push(formatElapsed(now - swarm.startedAt));
138
+ if (swarm.finalizing && swarm.finalizing.length > 0) {
139
+ parts.push(theme.fg("warning", `synthesizing ${swarm.finalizing.join(", ")}`));
140
+ }
141
+ if (swarm.dashboardUrl) parts.push(`${swarm.dashboardUrl}swarms/${swarm.id}`);
142
+ return `${parts.join(" ")} ${theme.fg("dim", "/agent-horde status | stop-hard | stop-clean [hN]")}`;
143
+ }
144
+
145
+ /** A swarm of one shows its run as before; a larger swarm shows one line per horde. */
146
+ export function widgetLines(swarm: WidgetSwarm, now: number, width: number, theme: Theme): string[] {
147
+ const frame = Math.floor(now / 100) % SPINNER.length;
148
+ const single = swarm.runs.length === 1 ? swarm.runs[0] : undefined;
149
+ const lines = single
150
+ ? runLines(single, now, frame, theme)
151
+ : [swarmHeader(swarm, now, theme), ...swarm.runs.map((run) => hordeLine(run, now, frame, theme))];
80
152
  return lines.map((line) => truncateToWidth(line, width));
81
153
  }
82
154
 
83
155
  export class HordeWidget {
84
- private readonly run: WidgetRun;
156
+ private readonly swarm: WidgetSwarm;
85
157
  private readonly theme: Theme;
86
158
  private readonly now: () => number;
87
159
 
88
- constructor(run: WidgetRun, theme: Theme, now: () => number = Date.now) {
89
- this.run = run;
160
+ constructor(swarm: WidgetSwarm, theme: Theme, now: () => number = Date.now) {
161
+ this.swarm = swarm;
90
162
  this.theme = theme;
91
163
  this.now = now;
92
164
  }
93
165
 
94
166
  render(width: number): string[] {
95
- return widgetLines(this.run, this.now(), width, this.theme);
167
+ return widgetLines(this.swarm, this.now(), width, this.theme);
96
168
  }
97
169
 
98
170
  invalidate(): void {}