/** * Wake notification rendering (§12.6). * * **R-UI-19 is the whole point of this file being separate from the note renderer.** * Wake payloads are `custom_message` entries and *must* reach the model — they are the * input to the turn they trigger. Notes are `custom` entries and must *not* — they are * output for the human, and re-feeding every note into the orchestrator's context on * every later turn is pure waste (R-ORCH-14). * * Concretely: this uses `registerMessageRenderer`, `agi-note` uses * `registerEntryRenderer`. Swapping them silently breaks either the wake (the model * never sees why it woke) or the context budget (every note replayed forever). */ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; import { Text } from "@earendil-works/pi-tui"; import type { WakeReason } from "../scheduler/index.ts"; import { WAKE_CUSTOM_TYPE } from "../scheduler/index.ts"; export interface WakeMessageDetails { reason: WakeReason; } const TITLES: Record = { worker_complete: "worker complete", worker_attention: "worker needs attention", worker_check: "worker trajectory check", tick: "scheduled check", sleep: "wait finished", user: "user", startup: "recovered work", }; /** R-UI-20: collapsed shows the headline, expanded shows the payload, capped. */ const COLLAPSED_LINES = 4; const EXPANDED_LINES = 40; export function wakeLines(text: string, expanded: boolean): string[] { const lines = text.split("\n").filter((line) => line.trim().length > 0); const budget = expanded ? EXPANDED_LINES : COLLAPSED_LINES; if (lines.length <= budget) return lines; return [...lines.slice(0, budget), expanded ? `… ${lines.length - budget} more line(s)` : "(ctrl+o to expand)"]; } export function registerWakeRenderer(pi: ExtensionAPI): void { pi.registerMessageRenderer(WAKE_CUSTOM_TYPE, (message, options, theme) => { const details = message.details; const reason = details?.reason ?? "tick"; const colour = reason === "worker_attention" ? "warning" : reason === "worker_complete" ? "success" : "muted"; const text = typeof message.content === "string" ? message.content : message.content.map((part) => (part.type === "text" ? part.text : "")).join(""); const body = wakeLines(text, options.expanded); return new Text([theme.fg(colour, `┌ ${TITLES[reason] ?? "update"}`), ...body.map((line) => `${theme.fg("muted", "│")} ${line}`)].join("\n"), 0, 0); }); }