Public: anyone can read this; the crew can change it.
The terminal dashboard's builders line says why builders are idle, with the same words as home #7733
Check · Review · Land · Live done — Landed and live in production.
mason/forge:mason/7665-the-terminal-dashboard-s-builders-line-s → main 1 commit fork 1 thread · 1 unresolved
2 files
changed +27
−2
src/tui/screens/dashboard.ts +11 −2
| @@ -112,7 +112,7 @@ import { firedAlarms } from "../../flow/alarms.ts"; | ||
| 112 | 112 | import { lastClearedNote } from "../../flow/clog_tick.ts"; |
| 113 | 113 | import { forgeCeiling } from "../../flow/ceiling.ts"; |
| 114 | 114 | import { alarmIdleCrew } from "../../flow/alarm_idle_crew.ts"; |
| 115 | import { idleLine, idleReason } from "../../crew/idle_reason.ts"; | |
| 115 | import { idleLine, idleReason, type IdleReason } from "../../crew/idle_reason.ts"; | |
| 116 | 116 | import { noReleaseAlarm } from "../../flow/alarm_no_release.ts"; |
| 117 | 117 | import { redMainAlarm } from "../../flow/alarm_red_main.ts"; |
| 118 | 118 | import { alarmStuck } from "../../flow/alarm_stuck.ts"; |
| @@ -345,7 +345,10 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen { | ||
| 345 | 345 | ? [] |
| 346 | 346 | : [ |
| 347 | 347 | ...idleLines(f.app, now), |
| 348 | ...kpiLines(readKpis(f.db, now, kpiForge(f.app)), commit === null ? null : { sha: commit, since: ago(BOOTED) }), | |
| 348 | ...withIdleWord( | |
| 349 | kpiLines(readKpis(f.db, now, kpiForge(f.app)), commit === null ? null : { sha: commit, since: ago(BOOTED) }), | |
| 350 | idleReason(f.app, now), | |
| 351 | ), | |
| 349 | 352 | vitalsLine(readVitals(f.db, now)), |
| 350 | 353 | ...firedAlarms(f.db, now, { ceilingUsd: forgeCeiling(f.app.config) }).map((a) => a.says), |
| 351 | 354 | ...(cleared === undefined ? [] : [cleared]), |
| @@ -1434,6 +1437,12 @@ export function dashboardScreen(f: Session, ctx: Ctx): Screen { | ||
| 1434 | 1437 | return screen; |
| 1435 | 1438 | } |
| 1436 | 1439 | |
| 1440 | /** The builders line ends ` · <word>` while they are idle, the word home prints (`idleReason`). */ | |
| 1441 | function withIdleWord(lines: string[], reason: IdleReason | null): string[] { | |
| 1442 | if (reason === null) return lines; | |
| 1443 | return lines.map((l) => (l.startsWith("Builders running ") ? `${l} · ${reason.word}` : l)); | |
| 1444 | } | |
| 1445 | ||
| 1437 | 1446 | /** Why the builders are idle, as the one line an admin's home reads; none while they are not. */ |
| 1438 | 1447 | function idleLines(app: App, now: Date): string[] { |
| 1439 | 1448 | const reason = idleReason(app, now); |
test/tui-dashboard-idle.test.ts +16
| @@ -75,3 +75,19 @@ test("the admin's home prints the reason's line; nobody else's does", async () = | ||
| 75 | 75 | expect(await firstScreen(alice)).toContain(idleLine(reason!)); |
| 76 | 76 | expect(await firstScreen(bob)).not.toContain("Builders idle"); |
| 77 | 77 | }); |
| 78 | ||
| 79 | test("the builders line ends with the reason's word while a slot is free; with every slot busy it has none", async () => { | |
| 80 | const repo = forge.db.query("SELECT id FROM repo WHERE name = 'svc'").get() as { id: string }; | |
| 81 | forge.db.query("UPDATE repo SET autonomy_ceiling_usd = 1000 WHERE id = ?1").run(repo.id); | |
| 82 | const reason = idleReason(forge, new Date()); | |
| 83 | expect(reason?.word).toBe("nothing ready"); | |
| 84 | const free = await firstScreen(alice); | |
| 85 | expect(free).toMatch(new RegExp(`Builders running .* · ${reason!.word}`)); | |
| 86 | ||
| 87 | const bot = getUserByName(forge.db, "idlebot")!; | |
| 88 | for (let i = 0; i < 2; i++) createRun(forge.db, { repoId: repo.id, kind: "agent", actorId: bot.id }); | |
| 89 | expect(idleReason(forge, new Date())).toBeNull(); | |
| 90 | const busy = await firstScreen(alice); | |
| 91 | expect(busy).toContain("Builders running"); | |
| 92 | expect(busy).not.toContain("nothing ready"); | |
| 93 | }); |
new above
offline
The startsWith("Builders running ") match ties this to kpiLines wording in src/flow/kpi_words.ts. If that wording changes, the word silently disappears. It is covered by the test today. A small exported constant for the prefix would be sturdier.