/** * The `session` tool's transcript block: the expanded rows one section per entity * group, and the one collapsed line a reader sees without expanding. * * The row grammar and the sanitization live in `rows.ts`; this module only decides * which rows an action shows and how the failed ones read. */ import type { Theme } from "@earendil-works/pi-coding-agent"; import { sanitizeLine } from "../../lib/format.ts"; import { SEPARATORS } from "../../lib/tui/glyphs.ts"; import type { TailResultInput, TailSection } from "../../lib/tui/tool.ts"; import { paint } from "../../lib/tui/tokens.ts"; import { type BusRow, busRowLine, count, sessionRows } from "./rows.ts"; import type { SessionDetails } from "./types.ts"; /** One section of the expanded result. */ function section(rows: BusRow[], label?: string): TailSection { return { label, rows, more: 0, rowLine: busRowLine }; } /** * The expanded result rows for one action, in the order a reader expects: the * board, the consumed mail, a conflict before the claim, released before * not-owned. Returns `undefined` when the action has nothing to list. */ export function resultSections(details: SessionDetails): TailResultInput | undefined { switch (details.action) { case "status": { const board = sessionRows(details.status); const claims = board.flatMap((row) => row.claims); const sections: TailSection[] = []; if (board.length > 0) sections.push(section(board.map((row) => ({ kind: "session", row })))); if (claims.length > 0) { sections.push( section( claims.map((claim) => ({ kind: "claim", claim })), "Claims", ), ); } return sections.length > 0 ? { kind: "sections", sections } : undefined; } case "inbox": { const messages = details.messages ?? []; if (messages.length === 0) return undefined; return { kind: "sections", sections: [section(messages.map((message) => ({ kind: "message", message })))] }; } case "claim": { const conflicts = details.conflicts ?? []; if (conflicts.length > 0) { return { kind: "sections", sections: [ section( conflicts.map((claim) => ({ kind: "claim", claim })), "Conflicts", ), ], }; } const claimed = details.claimed ?? []; if (claimed.length === 0) return undefined; return { kind: "sections", sections: [ section( claimed.map((label) => ({ kind: "result", label, tone: "ok" })), "Claimed", ), ], }; } case "release": { const released = details.released ?? []; const notOwned = details.notOwned ?? []; const sections: TailSection[] = []; if (released.length > 0) { sections.push( section( released.map((label) => ({ kind: "result", label, tone: "ok" })), "Released", ), ); } if (notOwned.length > 0) { sections.push( section( notOwned.map((label) => ({ kind: "result", label, tone: "warn" })), "Not yours", ), ); } return sections.length > 0 ? { kind: "sections", sections } : undefined; } case "send": { const delivered = details.delivered ?? []; if (delivered.length === 0) return undefined; return { kind: "sections", sections: [section(delivered.map((label) => ({ kind: "result", label, tone: "ok" })))], }; } case "join": return undefined; } } /** * The collapsed result line: the one thing the user needs without expanding. * * A conflict or a path this session does not own is worth the warning role; a * successful claim, release, or send is routine, so those stay calm. */ export function collapsedLine(theme: Theme, details: SessionDetails): string { const sep = SEPARATORS.item; switch (details.action) { case "status": { const board = sessionRows(details.status); const unread = board.reduce((total, row) => total + row.session.unread, 0); const claims = board.reduce((total, row) => total + row.claims.length, 0); const cells = [count(board.length, "session")]; if (unread > 0) cells.push(`${unread} unread`); cells.push(count(claims, "claim")); const watching = details.status?.watching; if (watching) cells.push(watching.wake ? "watch wake" : "watch on"); const line = paint(theme, "meta", cells.join(sep)); // A reader that has not joined is looking at a bus it is not on. if (details.status?.onBus === false) { return `${line}${paint(theme, "faint", sep)}${paint(theme, "warn", "not on the bus \u2014 session join puts you on it")}`; } return line; } case "send": { const delivered = details.delivered ?? []; if (delivered.length === 0) return paint(theme, "meta", "Delivered to nobody"); return paint(theme, "ok", `Delivered to ${delivered.map((label) => sanitizeLine(label)).join(", ")}`); } case "inbox": { const messages = details.messages ?? []; if (messages.length === 0) return paint(theme, "meta", "Inbox empty"); const senders = [...new Set(messages.map((message) => sanitizeLine(message.fromLabel)))]; return paint(theme, "meta", `${count(messages.length, "message")}${sep}${senders.join(", ")}`); } case "claim": { const conflicts = details.conflicts ?? []; if (conflicts.length > 0) { const [first] = conflicts; const more = conflicts.length - 1; const path = first ? sanitizeLine(first.path) : "a path"; const owner = first ? sanitizeLine(first.ownerLabel) : "a peer"; return paint( theme, "warn", `${count(conflicts.length, "conflict")}${sep}${path} held by ${owner}${more > 0 ? ` +${more} more` : ""}`, ); } const claimed = details.claimed ?? []; if (claimed.length === 0) return paint(theme, "meta", "Claimed nothing"); return paint(theme, "ok", `Claimed ${claimed.map((path) => sanitizeLine(path)).join(", ")}`); } case "release": { const released = details.released ?? []; const notOwned = details.notOwned ?? []; const parts: string[] = []; if (released.length > 0) parts.push(paint(theme, "ok", `Released ${released.map((path) => sanitizeLine(path)).join(", ")}`)); if (notOwned.length > 0) { parts.push(paint(theme, "warn", `Not yours: ${notOwned.map((path) => sanitizeLine(path)).join(", ")}`)); } return parts.length > 0 ? parts.join(paint(theme, "faint", sep)) : paint(theme, "meta", "Nothing to release"); } case "join": { const label = paint(theme, "ok", sanitizeLine(details.label ?? "")); const state = details.watching?.armed !== true ? paint(theme, "meta", "not listening") : paint(theme, "meta", details.watching.wake === true ? "listening, wake" : "listening"); return `${label}${paint(theme, "faint", sep)}${state}`; } } }