Public: anyone can read this; the crew can change it.
The terminal's System screen says how the forge is: the version and commit it runs, draining or not, the rollout and its incidents, as MCP `health` does #7304
Check · Review · Land · Live done — Landed and live in production.
torque/forge:torque/7296-the-terminal-s-system-screen-says-how-th → main 1 commit fork
3 files
changed +102
−5
src/tui/forge.ts +7 −2
| @@ -68,7 +68,8 @@ import { resilienceLines, resilienceScreen } from "./screens/resilience.ts"; | ||
| 68 | 68 | import { forgeCeiling } from "../flow/ceiling.ts"; |
| 69 | 69 | import { flowLines, flowScreen } from "./screens/flow.ts"; |
| 70 | 70 | import { readHeld } from "../flow/held.ts"; |
| 71 | import { memoryScreen, systemLines, systemScreen } from "./screens/system.ts"; | |
| 71 | import { forgeLines, memoryScreen, systemLines, systemScreen } from "./screens/system.ts"; | |
| 72 | import { readHealth } from "../actions/health.ts"; | |
| 72 | 73 | import { grantable, READS_KEPT, recentReads } from "../core/telemetry.ts"; |
| 73 | 74 | import { noteRead, setTelemetryGrant, telemetryReader } from "../actions/telemetry.ts"; |
| 74 | 75 | import { queuesFor, systemFor } from "../system/telemetry.ts"; |
| @@ -303,7 +304,11 @@ export function forgeCommands(session: Session): (ctx: Ctx) => Command[] { | ||
| 303 | 304 | id: "system", |
| 304 | 305 | title: "System", |
| 305 | 306 | hint: "the machine now: CPU, memory, disks, processes", |
| 306 | run: () => ctx.push(systemScreen(ctx, systemLines(liveMachine().read()))), | |
| 307 | run: () => ctx.push(systemScreen(ctx, [ | |
| 308 | ...forgeLines(readHealth(session.app, session.user)), | |
| 309 | "", | |
| 310 | ...systemLines(liveMachine().read()), | |
| 311 | ])), | |
| 307 | 312 | }); |
| 308 | 313 | commands.push({ |
| 309 | 314 | id: "telemetry", |
src/tui/screens/system.ts +29 −1
| @@ -6,6 +6,11 @@ | ||
| 6 | 6 | * It reads `live().read()`, the same view the page and the API serve. |
| 7 | 7 | * Read-only; Enter, Esc or q closes it. |
| 8 | 8 | * |
| 9 | * Above the machine, the forge itself (#7296): `forgeLines` reads | |
| 10 | * `readHealth`, the answer MCP `health` gives the same user — version, short | |
| 11 | * commit, serving or draining, and one more line only when a rollout is not | |
| 12 | * simply fine or the platform reports incidents. | |
| 13 | * | |
| 9 | 14 | * `memoryScreen` is `/system`'s other half (#2151): the forge's RSS beside |
| 10 | 15 | * its JS heap, an hour a row for the last 24 h, read through `memoryHours` |
| 11 | 16 | * like the page. The gate is the page's (`canSeeMemory`): anyone but a forge |
| @@ -14,12 +19,14 @@ | ||
| 14 | 19 | |
| 15 | 20 | import type { Ctx, Screen } from "../app/app.ts"; |
| 16 | 21 | import type { SystemView } from "../../system/view.ts"; |
| 22 | import type { readHealth } from "../../actions/health.ts"; | |
| 23 | import { rolloutLine, type ForgeHealth } from "../../kernel/guard.ts"; | |
| 17 | 24 | import { canSeeMemory, memoryHours } from "../../boards/memory.ts"; |
| 18 | 25 | import { binding } from "../app/keys.ts"; |
| 19 | 26 | import type { Session } from "../session.ts"; |
| 20 | 27 | import { bytes, spark } from "../../system/format.ts"; |
| 21 | 28 | import { style } from "../term/style.ts"; |
| 22 | import { pad, wrap } from "../term/width.ts"; | |
| 29 | import { pad, truncate, wrap } from "../term/width.ts"; | |
| 23 | 30 | import { dialog } from "../ui/widgets.ts"; |
| 24 | 31 | import { Scroll, drawLines, hitScroll } from "./common.ts"; |
| 25 | 32 | |
| @@ -49,6 +56,27 @@ export function duplicateLines(dups: SystemView["duplicates"], inner: number): s | ||
| 49 | 56 | const capOf = (g: SystemView["groups"][number]): string => |
| 50 | 57 | g.capBytes === null ? "" : `${bytes(g.rssBytes)} of ${bytes(g.capBytes)} ${g.capBy === "kernel" ? "kernel" : "watched"}`; |
| 51 | 58 | |
| 59 | /** | |
| 60 | * The forge as MCP `health` says it: the first line always, the second only | |
| 61 | * when there is something to say. A stranger's answer carries no rollout or | |
| 62 | * incidents, so it never has a second line. | |
| 63 | */ | |
| 64 | export function forgeLines(h: ReturnType<typeof readHealth>, inner = WIDTH - 6): string[] { | |
| 65 | const head = `forge ${h.version} · ${h.commit === null ? "no commit" : h.commit.slice(0, 7)} · ${h.draining ? "draining" : "serving"}`; | |
| 66 | if (!("incidents" in h)) return [head]; | |
| 67 | const full = h as ForgeHealth; | |
| 68 | const rollout = rolloutLine(full); | |
| 69 | // `stuck` and `held` are the rollout's own words; the rest are incidents. | |
| 70 | const others = full.incidents.filter((i) => i.key !== "stuck" && i.key !== "held"); | |
| 71 | const said = [ | |
| 72 | ...(rollout === null ? [] : [rollout]), | |
| 73 | ...(others.length === 0 | |
| 74 | ? [] | |
| 75 | : [`${others.length} incident${others.length === 1 ? "" : "s"}: ${others.map((i) => `${i.key}: ${i.message}`).join("; ")}`]), | |
| 76 | ]; | |
| 77 | return said.length === 0 ? [head] : [head, truncate(said.join(" · "), inner)]; | |
| 78 | } | |
| 79 | ||
| 52 | 80 | export function systemLines(v: SystemView): string[] { |
| 53 | 81 | const inner = WIDTH - 6; |
| 54 | 82 | const cores = v.cpu.cores.slice(0, CORES).map( |
test/tui-system.test.ts +66 −2
| @@ -4,15 +4,16 @@ | ||
| 4 | 4 | * guest get neither. |
| 5 | 5 | */ |
| 6 | 6 | import { afterAll, beforeAll, describe, expect, test } from "bun:test"; |
| 7 | import { mkdtemp, rm } from "node:fs/promises"; | |
| 7 | import { mkdtemp, rm, writeFile } from "node:fs/promises"; | |
| 8 | 8 | import { tmpdir } from "node:os"; |
| 9 | 9 | import { join } from "node:path"; |
| 10 | 10 | import { createAgent } from "../src/actions/crew.ts"; |
| 11 | import { readHealth } from "../src/actions/health.ts"; | |
| 11 | 12 | import { createApp, type App } from "../src/app.ts"; |
| 12 | 13 | import { recordMemory } from "../src/boards/memory.ts"; |
| 13 | 14 | import { createUser, type User } from "../src/core/users.ts"; |
| 14 | 15 | import { createForgeApp, forgeCommands, Session } from "../src/tui/forge.ts"; |
| 15 | import { memoryLines, memoryScreen } from "../src/tui/screens/system.ts"; | |
| 16 | import { forgeLines, memoryLines, memoryScreen } from "../src/tui/screens/system.ts"; | |
| 16 | 17 | import { headlessTerminal } from "../src/tui/term/terminal.ts"; |
| 17 | 18 | import { quietLogs } from "./helpers/globals.ts"; |
| 18 | 19 | import { settleApp } from "./helpers/tui.ts"; |
| @@ -99,3 +100,66 @@ describe("tui system memory", () => { | ||
| 99 | 100 | } |
| 100 | 101 | }); |
| 101 | 102 | }); |
| 103 | ||
| 104 | describe("tui system: the forge's own state (#7296)", () => { | |
| 105 | const SHA = "0123456789abcdef0123456789abcdef01234567"; | |
| 106 | const saved = ["FORGE_GUARD_DIR", "FORGE_VERSION", "FORGE_COMMIT"].map((k) => [k, process.env[k]] as const); | |
| 107 | let guard: string; | |
| 108 | ||
| 109 | beforeAll(async () => { | |
| 110 | guard = await mkdtemp(join(tmpdir(), "forge-tui-system-guard-")); | |
| 111 | process.env["FORGE_GUARD_DIR"] = guard; | |
| 112 | process.env["FORGE_VERSION"] = "v9.8.7"; | |
| 113 | process.env["FORGE_COMMIT"] = SHA; | |
| 114 | }); | |
| 115 | ||
| 116 | afterAll(async () => { | |
| 117 | for (const [k, v] of saved) v === undefined ? delete process.env[k] : (process.env[k] = v); | |
| 118 | await rm(guard, { recursive: true, force: true }); | |
| 119 | }); | |
| 120 | ||
| 121 | const seed = async (incidents: Record<string, string>) => { | |
| 122 | await writeFile(join(guard, "status.json"), JSON.stringify({ incidents })); | |
| 123 | await writeFile(join(guard, "checkedAt"), new Date().toISOString()); | |
| 124 | }; | |
| 125 | ||
| 126 | async function systemText(user: User): Promise<string> { | |
| 127 | const terminal = headlessTerminal(120, 50); | |
| 128 | const { app: tui, session } = createForgeApp(app, identity(user), { terminal, exitHooks: false }); | |
| 129 | tui.start(); | |
| 130 | forgeCommands(session)(tui.ctx).find((c) => c.id === "system")!.run(); | |
| 131 | await settleApp(tui); | |
| 132 | const text = tui.screen.toText(); | |
| 133 | tui.stop?.(); | |
| 134 | return text; | |
| 135 | } | |
| 136 | ||
| 137 | test("all well: one line with the version, the short commit and serving; nothing more", async () => { | |
| 138 | await seed({}); | |
| 139 | const lines = forgeLines(readHealth(app, carol)); | |
| 140 | expect(lines).toEqual(["forge 9.8.7 · 0123456 · serving"]); | |
| 141 | const text = await systemText(carol); | |
| 142 | expect(text).toContain("forge 9.8.7 · 0123456 · serving"); | |
| 143 | expect(text).not.toContain("incident"); | |
| 144 | }); | |
| 145 | ||
| 146 | test("a seeded incident adds exactly one line, in words", async () => { | |
| 147 | await seed({ down: "the edge answered 502" }); | |
| 148 | const lines = forgeLines(readHealth(app, carol)); | |
| 149 | expect(lines).toEqual(["forge 9.8.7 · 0123456 · serving", "1 incident: down: the edge answered 502"]); | |
| 150 | expect(await systemText(carol)).toContain("1 incident: down: the edge answered 502"); | |
| 151 | }); | |
| 152 | ||
| 153 | test("a blocked rollout and an incident share the one line", async () => { | |
| 154 | await seed({ stuck: "quota refused", "oom:web": "killed" }); | |
| 155 | expect(forgeLines(readHealth(app, carol), 20)[1]).toBe("rollout blocked: qu…"); | |
| 156 | const lines = forgeLines(readHealth(app, carol)); | |
| 157 | expect(lines.length).toBe(2); | |
| 158 | expect(lines[1]).toBe("rollout blocked: quota refused · 1 incident: oom:web: killed"); | |
| 159 | }); | |
| 160 | ||
| 161 | test("a stranger's answer, as MCP gives it, never has a second line", async () => { | |
| 162 | await seed({ down: "the edge answered 502" }); | |
| 163 | expect(forgeLines(readHealth(app, undefined))).toEqual(["forge 9.8.7 · 0123456 · serving"]); | |
| 164 | }); | |
| 165 | }); |
new above
offline