parawatch 0.0.0-stage → 0.2.0

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.
@@ -0,0 +1,146 @@
1
+ /**
2
+ * What each session did on one day, from the event log and the agent logs: the work it claimed,
3
+ * cards it opened and what came of them, jobs it declared, news on its pull requests, and how it
4
+ * said it ended.
5
+ */
6
+ import { collectRuns } from "./runs.js";
7
+ import { fold } from "./state.js";
8
+ import { readEvents } from "./store.js";
9
+ import { prChangeText } from "./format.js";
10
+ /** Local midnight to local midnight. */
11
+ export function dayRange(date) {
12
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(date))
13
+ throw new Error(`"${date}" is not a date (use YYYY-MM-DD)`);
14
+ const start = new Date(`${date}T00:00:00`).getTime();
15
+ const end = new Date(start);
16
+ end.setDate(end.getDate() + 1);
17
+ return [start, end.getTime()];
18
+ }
19
+ export function localDate(t = Date.now()) {
20
+ const d = new Date(t);
21
+ return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}-${String(d.getDate()).padStart(2, "0")}`;
22
+ }
23
+ export function digest(date, now = Date.now()) {
24
+ const [from, to] = dayRange(date);
25
+ const events = readEvents();
26
+ const state = fold(events);
27
+ const inDay = (e) => {
28
+ const t = Date.parse(e.ts);
29
+ return t >= from && t < to;
30
+ };
31
+ const sessions = new Map();
32
+ const sessionKey = (id) => {
33
+ if (!id)
34
+ return "";
35
+ const c = state.claims.find((x) => id.startsWith(x.session) || x.session.startsWith(id));
36
+ return c?.session ?? id;
37
+ };
38
+ const get = (id) => {
39
+ const k = sessionKey(id);
40
+ let s = sessions.get(k);
41
+ if (!s) {
42
+ const c = state.claims.find((x) => x.session === k);
43
+ s = { session: k, title: c?.title ?? (k ? k.slice(0, 8) : "no session"), project: c?.project, task: c?.task, cards: [], runs: [], prs: [] };
44
+ sessions.set(k, s);
45
+ }
46
+ return s;
47
+ };
48
+ // agent logs: what was active that day
49
+ const sinceHours = Math.max(1, Math.ceil((now - from) / 3_600_000));
50
+ const observed = collectRuns(state, { sinceHours, includeEnded: true, keepRepeats: true }, now).filter((r) => r.kind === "session" && r.lastActivity && Date.parse(r.lastActivity) >= from && (!r.startedAt || Date.parse(r.startedAt) < to));
51
+ for (const r of observed) {
52
+ const s = get(r.id);
53
+ s.title = r.title;
54
+ s.project ??= r.project;
55
+ s.task ??= r.task;
56
+ s.recap = r.recap;
57
+ s.lastActivity = r.lastActivity;
58
+ }
59
+ for (const e of events) {
60
+ if (!inDay(e))
61
+ continue;
62
+ switch (e.type) {
63
+ case "run.claim": {
64
+ const s = get(e.claim.session);
65
+ s.project = e.claim.project;
66
+ s.task = e.claim.task ?? s.task;
67
+ if (e.claim.title)
68
+ s.title = e.claim.title;
69
+ if (e.claim.done)
70
+ s.done = e.claim.done;
71
+ break;
72
+ }
73
+ case "card.open":
74
+ case "card.answer":
75
+ case "card.close": {
76
+ const id = e.type === "card.open" ? e.card.id : e.card;
77
+ const card = state.cards.get(id);
78
+ if (!card)
79
+ break;
80
+ const s = get(card.session);
81
+ s.project ??= card.project;
82
+ let row = s.cards.find((c) => c.id === id);
83
+ if (!row)
84
+ s.cards.push((row = { id, title: card.title, events: [] }));
85
+ if (e.type === "card.open")
86
+ row.events.push("opened");
87
+ else if (e.type === "card.answer") {
88
+ const choice = e.choice !== undefined ? card.options?.[e.choice - 1] : undefined;
89
+ row.events.push(`answered${choice ? `: ${choice}` : e.comment ? `: ${e.comment}` : ""}`);
90
+ }
91
+ else
92
+ row.events.push(`closed${e.note ? `: ${e.note}` : ""}`);
93
+ break;
94
+ }
95
+ case "run.start":
96
+ case "run.end": {
97
+ const id = e.type === "run.start" ? e.run.id : e.run;
98
+ const run = state.runs.get(id);
99
+ if (!run)
100
+ break;
101
+ const s = get(run.parent);
102
+ s.project ??= run.project;
103
+ if (!s.runs.some((x) => x.id === id))
104
+ s.runs.push({ id, title: run.title, status: run.status, note: run.note });
105
+ break;
106
+ }
107
+ case "pr.change": {
108
+ const link = state.prLinks.get(e.pr.key);
109
+ const s = get(link?.session);
110
+ s.project ??= link?.project;
111
+ let row = s.prs.find((p) => p.key === e.pr.key);
112
+ if (!row)
113
+ s.prs.push((row = { key: e.pr.key, url: e.pr.url, title: e.title, changes: [] }));
114
+ row.changes.push(prChangeText(e.change, e.by) + (e.text ? `: ${e.text}` : ""));
115
+ break;
116
+ }
117
+ }
118
+ }
119
+ const list = [...sessions.values()].filter((s) => s.session || s.cards.length || s.runs.length || s.prs.length);
120
+ list.sort((a, b) => (a.project ?? "~").localeCompare(b.project ?? "~") || (a.lastActivity ?? "").localeCompare(b.lastActivity ?? ""));
121
+ return { date, sessions: list };
122
+ }
123
+ export function digestText(d) {
124
+ const out = [d.date];
125
+ if (!d.sessions.length)
126
+ return `${d.date}\nNothing recorded.`;
127
+ let project = null;
128
+ for (const s of d.sessions) {
129
+ if (s.project !== project) {
130
+ project = s.project;
131
+ out.push("", project ?? "other");
132
+ }
133
+ out.push(` ${s.task ? `${s.task} · ` : ""}${s.title}`);
134
+ const end = s.done ? `done: ${s.done}` : s.recap ? `last: ${s.recap}` : undefined;
135
+ if (end)
136
+ out.push(` ${end}`);
137
+ for (const c of s.cards)
138
+ out.push(` card ${c.id} "${c.title}": ${c.events.join(", ")}`);
139
+ for (const r of s.runs)
140
+ out.push(` job ${r.id} "${r.title}": ${r.status}${r.note ? ` (${r.note})` : ""}`);
141
+ for (const p of s.prs)
142
+ out.push(` PR ${p.key.replace(/^[^/]+\//, "")}${p.title ? ` "${p.title}"` : ""}: ${p.changes.join(", ")}`);
143
+ }
144
+ return out.join("\n");
145
+ }
146
+ //# sourceMappingURL=digest.js.map
@@ -0,0 +1,70 @@
1
+ import fs from "node:fs";
2
+ import { eventsPath, homeDir, parseEventLines } from "./store.js";
3
+ /**
4
+ * Call `onEvent` for every event appended after `afterSeq` (or after the current end when it is
5
+ * undefined). Watches the store folder and also checks once a second. Returns a stop function.
6
+ */
7
+ export function followEvents(onEvent, afterSeq) {
8
+ const file = eventsPath();
9
+ let offset = 0;
10
+ let lastSeq = afterSeq ?? Infinity;
11
+ try {
12
+ const text = fs.readFileSync(file, "utf8");
13
+ offset = Buffer.byteLength(text);
14
+ const events = parseEventLines(text);
15
+ const end = events[events.length - 1]?.seq ?? 0;
16
+ if (afterSeq === undefined)
17
+ lastSeq = end;
18
+ else {
19
+ for (const ev of events)
20
+ if (ev.seq > afterSeq)
21
+ onEvent(ev);
22
+ lastSeq = Math.max(afterSeq, end);
23
+ }
24
+ }
25
+ catch {
26
+ lastSeq = afterSeq ?? 0;
27
+ }
28
+ const pump = () => {
29
+ let size = 0;
30
+ try {
31
+ size = fs.statSync(file).size;
32
+ }
33
+ catch {
34
+ return;
35
+ }
36
+ if (size < offset)
37
+ offset = 0; // file replaced
38
+ if (size === offset)
39
+ return;
40
+ const fd = fs.openSync(file, "r");
41
+ const buf = Buffer.alloc(size - offset);
42
+ fs.readSync(fd, buf, 0, buf.length, offset);
43
+ fs.closeSync(fd);
44
+ const text = buf.toString("utf8");
45
+ const cut = text.lastIndexOf("\n");
46
+ if (cut < 0)
47
+ return;
48
+ offset += Buffer.byteLength(text.slice(0, cut + 1));
49
+ for (const ev of parseEventLines(text.slice(0, cut + 1))) {
50
+ if (ev.seq > lastSeq) {
51
+ onEvent(ev);
52
+ lastSeq = ev.seq;
53
+ }
54
+ }
55
+ };
56
+ let watcher;
57
+ try {
58
+ fs.mkdirSync(homeDir(), { recursive: true });
59
+ watcher = fs.watch(homeDir(), () => pump());
60
+ }
61
+ catch {
62
+ watcher = undefined; // the timer alone still works
63
+ }
64
+ const timer = setInterval(pump, 1000);
65
+ return () => {
66
+ clearInterval(timer);
67
+ watcher?.close();
68
+ };
69
+ }
70
+ //# sourceMappingURL=follow.js.map
@@ -0,0 +1,351 @@
1
+ export function ago(iso, now = Date.now()) {
2
+ if (!iso)
3
+ return "?";
4
+ const s = Math.max(0, Math.round((now - Date.parse(iso)) / 1000));
5
+ if (s < 60)
6
+ return `${s}s`;
7
+ const m = Math.round(s / 60);
8
+ if (m < 60)
9
+ return `${m}m`;
10
+ const h = Math.round(m / 60);
11
+ if (h < 48)
12
+ return `${h}h`;
13
+ return `${Math.round(h / 24)}d`;
14
+ }
15
+ export function view(opts = {}) {
16
+ const out = process.stdout;
17
+ const color = process.env.NO_COLOR ? false : process.env.FORCE_COLOR ? true : Boolean(out.isTTY);
18
+ const width = out.columns || Number(process.env.COLUMNS) || 120;
19
+ return { color, width: Math.max(50, width), wide: Boolean(opts.wide), all: Boolean(opts.all) };
20
+ }
21
+ const PLAIN = { color: false, width: 120, wide: false, all: false };
22
+ const SGR = { bold: "1", dim: "2", red: "31", green: "32", yellow: "33", blue: "34", magenta: "35", cyan: "36" };
23
+ function paint(v, s, ...codes) {
24
+ if (!v.color || !s || !codes.length)
25
+ return s;
26
+ return `\x1b[${codes.map((c) => SGR[c]).join(";")}m${s}\x1b[0m`;
27
+ }
28
+ function charWidth(cp) {
29
+ if (cp < 32 || (cp >= 0x7f && cp < 0xa0))
30
+ return 0;
31
+ if ((cp >= 0x300 && cp <= 0x36f) || (cp >= 0x200b && cp <= 0x200f) || (cp >= 0xfe00 && cp <= 0xfe0f))
32
+ return 0;
33
+ if ((cp >= 0x1100 && cp <= 0x115f) ||
34
+ (cp >= 0x2e80 && cp <= 0xa4cf) ||
35
+ (cp >= 0xac00 && cp <= 0xd7a3) ||
36
+ (cp >= 0xf900 && cp <= 0xfaff) ||
37
+ (cp >= 0xfe30 && cp <= 0xfe4f) ||
38
+ (cp >= 0xff00 && cp <= 0xff60) ||
39
+ (cp >= 0xffe0 && cp <= 0xffe6) ||
40
+ (cp >= 0x1f300 && cp <= 0x1faff) ||
41
+ (cp >= 0x20000 && cp <= 0x3fffd))
42
+ return 2;
43
+ return 1;
44
+ }
45
+ /** Columns a string takes in a terminal (CJK and emoji count as two). */
46
+ export function textWidth(s) {
47
+ let w = 0;
48
+ for (const ch of s)
49
+ w += charWidth(ch.codePointAt(0));
50
+ return w;
51
+ }
52
+ /** Cut to at most `max` columns, ending with … when cut. */
53
+ export function fit(s, max) {
54
+ if (max <= 0)
55
+ return "";
56
+ if (textWidth(s) <= max)
57
+ return s;
58
+ let w = 0;
59
+ let out = "";
60
+ for (const ch of s) {
61
+ const c = charWidth(ch.codePointAt(0));
62
+ if (w + c > max - 1)
63
+ break;
64
+ out += ch;
65
+ w += c;
66
+ }
67
+ return out + "…";
68
+ }
69
+ function pad(s, n) {
70
+ return s + " ".repeat(Math.max(0, n - textWidth(s)));
71
+ }
72
+ /** One line of plain text from an agent message: no markdown, no line breaks. */
73
+ export function clean(s) {
74
+ return s
75
+ .replace(/\[([^\]]*)\]\([^)]*\)/g, "$1")
76
+ .replace(/\*\*|`/g, "")
77
+ .replace(/^\s*(?:[-*>]|#+)\s+/gm, "")
78
+ .replace(/\s+/g, " ")
79
+ .trim();
80
+ }
81
+ // ------------------------------------------------------------------ cards
82
+ const KIND_LABEL = { decide: "decide", try: "try", block: "blocked", todo: "to do" };
83
+ const KIND_PAINT = { decide: "yellow", try: "cyan", block: "red", todo: "blue" };
84
+ /** When the matter came up: `since` when it is older than the card, else when the card opened. */
85
+ export function cardStart(c) {
86
+ return c.since && c.since < c.openedAt ? c.since : c.openedAt;
87
+ }
88
+ /** Cards as aligned rows: id, kind, project, title, age. */
89
+ export function cardRows(cards, v = PLAIN, now = Date.now(), indent = "", extra = {}) {
90
+ const idW = Math.max(3, ...cards.map((c) => c.id.length));
91
+ const projW = Math.min(16, Math.max(0, ...cards.map((c) => textWidth(c.project + (c.task ? `/${c.task}` : "")))));
92
+ const noteOf = (c) => (c.status === "answered" ? " · answered" : c.status === "closed" ? " · closed" : "") + (extra[c.id] ? ` · ${extra[c.id]}` : "");
93
+ const fixed = textWidth(indent) + idW + 2 + 7 + 2 + projW + 2 + 2 + 4;
94
+ const titleW = Math.min(v.width - fixed, Math.max(0, ...cards.map((c) => textWidth(c.title + noteOf(c)))));
95
+ return cards.map((c) => {
96
+ const proj = c.project + (c.task ? `/${c.task}` : "");
97
+ const age = ago(cardStart(c), now).padStart(4);
98
+ const note = noteOf(c);
99
+ const title = fit(c.title, titleW - textWidth(note));
100
+ const dim = c.status !== "open";
101
+ return [
102
+ indent,
103
+ paint(v, pad(c.id, idW), "bold"),
104
+ " ",
105
+ paint(v, pad(KIND_LABEL[c.kind], 7), dim ? "dim" : KIND_PAINT[c.kind]),
106
+ " ",
107
+ pad(fit(proj, projW), projW),
108
+ " ",
109
+ paint(v, pad(title + note, titleW), ...(dim ? ["dim"] : [])),
110
+ " ",
111
+ paint(v, age, "dim"),
112
+ ].join("");
113
+ });
114
+ }
115
+ export function cardLine(c, now = Date.now()) {
116
+ return cardRows([c], PLAIN, now)[0].trimEnd();
117
+ }
118
+ export function cardDetail(c) {
119
+ const out = [];
120
+ out.push(`${c.id} · ${KIND_LABEL[c.kind]} · ${c.project}${c.task ? ` [${c.task}]` : ""} · ${c.status}`);
121
+ out.push(c.title);
122
+ if (c.body)
123
+ out.push("", c.body);
124
+ if (c.options?.length) {
125
+ out.push("");
126
+ c.options.forEach((o, i) => out.push(` ${i + 1}. ${o}${c.recommend === i + 1 ? " (recommended)" : ""}`));
127
+ }
128
+ if (c.action)
129
+ out.push("", `Runs on yes: ${c.action}`);
130
+ if (c.preview)
131
+ out.push("", "Preview:", ...c.preview.split("\n").map((l) => ` ${l}`));
132
+ if (c.look)
133
+ out.push("", `Look at: ${c.look}`);
134
+ for (const o of c.open ?? [])
135
+ out.push(`Open: ${o}`);
136
+ for (const a of c.attach ?? [])
137
+ out.push(`Attached: ${a}`);
138
+ if (c.from || c.session)
139
+ out.push(`From: ${[c.from, c.session ? `session ${c.session.slice(0, 8)}` : ""].filter(Boolean).join(", ")}`);
140
+ out.push(`Opened: ${new Date(c.openedAt).toLocaleString()}${c.since ? `, first came up ${new Date(c.since).toLocaleDateString()}` : ""}`);
141
+ for (const a of c.answers) {
142
+ const choice = a.choice !== undefined ? ` chose ${a.choice}${c.options?.[a.choice - 1] ? ` (${c.options[a.choice - 1]})` : ""}` : "";
143
+ out.push("", `${a.by}${choice}${a.comment ? `: ${a.comment}` : ""}`);
144
+ for (const f of a.attach ?? [])
145
+ out.push(` attached ${f}`);
146
+ }
147
+ for (const m of c.comments)
148
+ out.push(`${m.by}: ${m.text}`);
149
+ if (c.closeNote)
150
+ out.push("", `Closed: ${c.closeNote}`);
151
+ return out.join("\n");
152
+ }
153
+ const PR_WORDS = {
154
+ review: "new review",
155
+ approved: "approved",
156
+ changes_requested: "changes requested",
157
+ commented: "new comment",
158
+ ci_failed: "CI failed",
159
+ ci_passed: "CI passed",
160
+ queued: "entered the merge queue",
161
+ queue_removed: "left the merge queue without merging",
162
+ conflict: "has a merge conflict",
163
+ merged: "merged",
164
+ closed: "closed without merging",
165
+ note: "note",
166
+ };
167
+ export function prChangeText(change, by) {
168
+ const w = PR_WORDS[change] ?? change;
169
+ return by && by !== "github" && by !== "external" ? `${w} by ${by}` : w;
170
+ }
171
+ export function inboxLine(i) {
172
+ if (i.kind === "pr" && i.pr) {
173
+ const p = i.pr;
174
+ return `[${p.project ?? "?"}] PR ${p.ref.repo}#${p.ref.number}${p.title ? ` "${p.title}"` : ""}: ${prChangeText(p.change, i.by)}${p.text ? `; ${p.text}` : ""} (${p.ref.url})`;
175
+ }
176
+ const card = i.card;
177
+ const head = `[${card.project}] ${card.id} "${card.title}"`;
178
+ if (i.kind === "comment")
179
+ return `${head} comment: ${i.comment}`;
180
+ const parts = [];
181
+ if (i.choice !== undefined)
182
+ parts.push(`chose ${i.choice}${i.choiceText ? ` (${i.choiceText})` : ""}`);
183
+ if (i.comment)
184
+ parts.push(`said: ${i.comment}`);
185
+ if (i.attach?.length)
186
+ parts.push(`attached: ${i.attach.join(", ")}`);
187
+ return `${head} ${parts.join("; ")}`;
188
+ }
189
+ const STATE_MARK = {
190
+ working: "●",
191
+ waiting: "◐",
192
+ idle: "○",
193
+ done: "✓",
194
+ stalled: "!",
195
+ failed: "✗",
196
+ unknown: "?",
197
+ };
198
+ const STATE_PAINT = {
199
+ working: ["green"],
200
+ waiting: ["yellow"],
201
+ idle: ["dim"],
202
+ done: ["dim"],
203
+ stalled: ["red"],
204
+ failed: ["red"],
205
+ unknown: ["dim"],
206
+ };
207
+ const ENDED = new Set(["done", "idle"]);
208
+ // ------------------------------------------------------------------ pull requests
209
+ function prLabel(p) {
210
+ return `${p.repo.split("/")[1]}#${p.number}`;
211
+ }
212
+ /** Open pull requests: what each waits on, for how long, and the session that owns it. */
213
+ export function prRows(prs, v = PLAIN, now = Date.now(), indent = "") {
214
+ const idW = Math.max(4, ...prs.map((p) => textWidth(prLabel(p))));
215
+ const waitW = Math.min(30, Math.max(6, ...prs.map((p) => textWidth(waitText(p)))));
216
+ const ownW = Math.min(24, Math.max(0, ...prs.map((p) => textWidth(ownerText(p)))));
217
+ const fixed = textWidth(indent) + idW + 2 + waitW + 2 + 4 + 2 + (ownW ? ownW + 2 : 0);
218
+ const titleW = Math.max(8, v.width - fixed);
219
+ return prs.map((p) => {
220
+ const age = ago(p.since, now).padStart(4);
221
+ return [
222
+ indent,
223
+ paint(v, pad(prLabel(p), idW), "bold"),
224
+ " ",
225
+ paint(v, pad(fit(waitText(p), waitW), waitW), ...(p.stalled ? ["red"] : ["yellow"])),
226
+ " ",
227
+ paint(v, age, ...(p.stalled ? ["red"] : ["dim"])),
228
+ " ",
229
+ ownW ? paint(v, pad(fit(ownerText(p), ownW), ownW), "dim") + " " : "",
230
+ fit(p.title, titleW),
231
+ ].join("").trimEnd();
232
+ });
233
+ }
234
+ export function waitText(p) {
235
+ if (p.reviewRequested && !p.mine)
236
+ return `${p.stalled ? "long " : ""}waiting for your review`;
237
+ const who = p.who?.length && p.waitingOn === "review" ? ` (${p.who.join(", ")})` : "";
238
+ return `${p.stalled ? "stuck in " : ""}${p.waitingOn}${who}`;
239
+ }
240
+ function ownerText(p) {
241
+ return p.sessionTitle ?? (p.project ? p.project + (p.task ? `/${p.task}` : "") : "");
242
+ }
243
+ /** Sessions that asked the human something in chat instead of on a card. */
244
+ export function askRows(runs, v = PLAIN, now = Date.now(), indent = "") {
245
+ const asking = runs.filter((r) => r.asks);
246
+ const nameW = Math.min(32, Math.max(8, ...asking.map((r) => textWidth(r.title))));
247
+ return asking.map((r) => {
248
+ const age = ago(r.asks.at ?? r.lastActivity, now).padStart(4);
249
+ const room = Math.max(8, v.width - textWidth(indent) - nameW - 2 - 4 - 2 - 10);
250
+ const where = r.project ? `${r.project}` : "other";
251
+ return [
252
+ indent,
253
+ paint(v, pad(fit(r.title, nameW), nameW), "bold"),
254
+ " ",
255
+ paint(v, age, "dim"),
256
+ " ",
257
+ paint(v, fit(clean(r.asks.text), room), ...(r.asks.blocked ? ["red"] : ["yellow"])),
258
+ " ",
259
+ paint(v, `(${where})`, "dim"),
260
+ ].join("");
261
+ });
262
+ }
263
+ /** Finished subagents older than this fold into one "N finished" line. */
264
+ const FOLD_AFTER_MS = 5 * 60_000;
265
+ export const OTHER = "other";
266
+ function whatText(r) {
267
+ if (r.error)
268
+ return `unreadable: ${r.error}`;
269
+ if (r.done)
270
+ return `done · ${clean(r.done)}`;
271
+ if (r.asks)
272
+ return `${r.asks.blocked ? "blocked" : "asks"}: ${clean(r.asks.text)}`;
273
+ const a = clean((r.state === "idle" && r.recap) || r.action || "");
274
+ if (r.state === "stalled")
275
+ return a ? `stalled · ${a}` : "stalled";
276
+ return a;
277
+ }
278
+ const SOURCE_NOTE = { edits: "~edits", prompt: "~prompt" };
279
+ /** Runs grouped by project, children drawn as a tree under their parent, one terminal line each. */
280
+ export function runsTree(runs, v = PLAIN, now = Date.now(), indent = "") {
281
+ const groups = new Map();
282
+ for (const r of runs) {
283
+ const k = r.project ?? OTHER;
284
+ if (!groups.has(k))
285
+ groups.set(k, []);
286
+ groups.get(k).push(r);
287
+ }
288
+ const names = [...groups.keys()].sort((a, b) => (a === OTHER ? 1 : b === OTHER ? -1 : a.localeCompare(b)));
289
+ // rows first, so every group shares one set of columns
290
+ const blocks = [];
291
+ for (const name of names) {
292
+ const rs = groups.get(name);
293
+ const ids = new Set(rs.map((r) => r.id));
294
+ const rows = [];
295
+ const walk = (r, prefix, cont) => {
296
+ rows.push({ prefix, run: r });
297
+ const kids = rs.filter((x) => x.parent === r.id);
298
+ const shown = v.all ? kids : kids.filter((k) => !(ENDED.has(k.state) && now - Date.parse(k.lastActivity ?? "") > FOLD_AFTER_MS));
299
+ const folded = kids.filter((k) => !shown.includes(k));
300
+ const items = [...shown];
301
+ if (folded.length)
302
+ items.push({ count: folded.length, newest: folded.map((k) => k.lastActivity ?? "").sort().pop() });
303
+ items.forEach((it, i) => {
304
+ const last = i === items.length - 1;
305
+ const p = cont + (last ? "└ " : "├ ");
306
+ const c = cont + (last ? " " : "│ ");
307
+ if (it && "id" in it)
308
+ walk(it, p, c);
309
+ else
310
+ rows.push({ prefix: p, folded: it });
311
+ });
312
+ };
313
+ for (const r of rs.filter((r) => !r.parent || !ids.has(r.parent)))
314
+ walk(r, indent + " ", indent + " ");
315
+ blocks.push({ name, rows });
316
+ }
317
+ const all = blocks.flatMap((b) => b.rows);
318
+ const nameW = Math.min(Math.max(24, Math.floor(v.width * 0.4)), Math.max(12, ...all.map((row) => textWidth(row.prefix) + 2 + textWidth(row.run?.title ?? ""))));
319
+ const wideW = v.wide ? 24 : 0;
320
+ const out = [];
321
+ for (const { name, rows } of blocks) {
322
+ if (name === OTHER)
323
+ out.push(indent + paint(v, "other", "bold") + paint(v, " not in a registered project", "dim"));
324
+ else
325
+ out.push(indent + paint(v, name, "bold"));
326
+ for (const row of rows) {
327
+ const lead = paint(v, row.prefix, "dim");
328
+ const room = nameW - textWidth(row.prefix) - 2;
329
+ if (row.folded) {
330
+ const label = `✓ ${row.folded.count} finished`;
331
+ out.push(lead + paint(v, pad(fit(label, room + 2), room + 2), "dim") + " " + paint(v, ago(row.folded.newest, now).padStart(4), "dim"));
332
+ continue;
333
+ }
334
+ const r = row.run;
335
+ const ended = ENDED.has(r.state);
336
+ const titlePaint = r.role === "lead" ? ["bold"] : ended ? ["dim"] : [];
337
+ const mark = paint(v, STATE_MARK[r.state], ...STATE_PAINT[r.state]);
338
+ const label = r.title + (r.repeats ? ` ×${r.repeats + 1}` : "") + (r.prs?.length ? ` (${r.prs.length} PR)` : "");
339
+ const title = paint(v, pad(fit(label, room), room), ...titlePaint);
340
+ const age = paint(v, ago(r.lastActivity, now).padStart(4), "dim");
341
+ const src = (r.projectSource && SOURCE_NOTE[r.projectSource]) || "";
342
+ const wide = v.wide ? " " + paint(v, pad(fit(`${r.kind === "subagent" ? r.model ?? "subagent" : r.runtime} ${r.id.slice(0, 8)}${src ? " " + src : ""}`, wideW - 2), wideW - 2), "dim") : "";
343
+ const restW = v.width - nameW - 2 - 4 - wideW - 2;
344
+ const whatPaint = r.error || r.state === "stalled" || r.state === "failed" || r.asks?.blocked ? ["red"] : r.asks ? ["yellow"] : ended ? ["dim"] : [];
345
+ const what = restW >= 8 ? " " + paint(v, fit(whatText(r), restW), ...whatPaint) : "";
346
+ out.push(`${lead}${mark} ${title} ${age}${wide}${what}`.trimEnd());
347
+ }
348
+ }
349
+ return out.join("\n");
350
+ }
351
+ //# sourceMappingURL=format.js.map