Something went wrong. Try again.
fork of prime agent with changes i want but are not upstreamable probqbly (and some fixes)
Something went wrong. Try again.
5.3 kB · 161 lines
TypeScript
at main
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162import { Box, type Component, Markdown, Text, visibleWidth } from "@earendil-works/pi-tui";import type { AgentConnectionSideQuestionEvent } from "../../agent-connection/types.js";import { getMarkdownTheme, theme } from "../theme/theme.js";
interface SideQuestionTurnState { kind: "turn"; event: AgentConnectionSideQuestionEvent; /** Follow-up questions render as a standard user-message bubble; the first turn keeps the /btw header. */ questionBubble: Box | undefined; answer: Markdown;}
/** A ! / !! run inside the pane, rendered by the same component the main thread uses. */interface SideQuestionBashState { kind: "bash"; component: Component & { setExpanded?(expanded: boolean): void }; running: boolean;}
export class SideQuestionComponent implements Component { private readonly paddingX: number; private readonly entries: (SideQuestionTurnState | SideQuestionBashState)[] = []; private expanded = false;
constructor(event: AgentConnectionSideQuestionEvent, paddingX = 2) { this.paddingX = Math.max(2, paddingX); this.addTurn(event); }
addTurn(event: AgentConnectionSideQuestionEvent): void { let questionBubble: Box | undefined; if (this.entries.length > 0) { questionBubble = new Box(this.paddingX, 1, (content: string) => theme.getUserMessageBackgroundColor()(content), ); questionBubble.addChild( new Markdown(event.question, 0, 0, getMarkdownTheme(), { color: (content: string) => theme.fg("userMessageText", content), }), ); } const answer = new Markdown("", this.paddingX, 0, getMarkdownTheme(), { color: (content: string) => theme.fg("userMessageText", content), }); answer.setText(event.answer); this.entries.push({ kind: "turn", event, questionBubble, answer }); }
addBash(component: SideQuestionBashState["component"]): void { component.setExpanded?.(this.expanded); this.entries.push({ kind: "bash", component, running: true }); }
setExpanded(expanded: boolean): void { this.expanded = expanded; for (const entry of this.entries) { if (entry.kind === "bash") entry.component.setExpanded?.(expanded); } }
finishBash(): void { for (const entry of this.entries) { if (entry.kind === "bash") { entry.running = false; } } }
update(event: AgentConnectionSideQuestionEvent): void { const turn = this.entries.find( (candidate): candidate is SideQuestionTurnState => candidate.kind === "turn" && candidate.event.id === event.id, ); if (!turn) { return; } turn.event = event; turn.answer.setText(event.answer); }
invalidate(): void { for (const entry of this.entries) { if (entry.kind === "bash") { entry.component.invalidate(); } else { entry.questionBubble?.invalidate(); entry.answer.invalidate(); } } }
render(width: number): string[] { const blank = " ".repeat(Math.max(1, width)); const lines: string[] = []; const pushSurfaced = (raw: string[]) => { for (const line of raw) { lines.push(this.applySurface(line, width)); } };
pushSurfaced([blank]); for (const entry of this.entries) { if (entry.kind === "bash") { pushSurfaced([...entry.component.render(width), blank]); continue; } if (entry.questionBubble) { // Bubble lines are already fully painted with the user-message background. lines.push(...entry.questionBubble.render(width)); } else { const question = new Text( `${theme.fg("accent", "/btw")} ${theme.fg("userMessageText", entry.event.question)}`, this.paddingX, 0, ).render(width); pushSurfaced(question); } pushSurfaced([blank, ...this.renderAnswer(entry, width), blank]); } pushSurfaced([...this.renderHint(width), blank]); return lines; }
private renderAnswer(turn: SideQuestionTurnState, width: number): string[] { const lines: string[] = []; if (turn.event.answer) { lines.push(...turn.answer.render(width)); } if (turn.event.errorMessage) { // A turn can fail after streaming partial output; show both. lines.push(...new Text(theme.fg("error", turn.event.errorMessage), this.paddingX, 0).render(width)); } if (lines.length > 0) { return lines; } if (turn.event.status === "cancelled") { return new Text(theme.fg("userMessageText", "Cancelled"), this.paddingX, 0).render(width); } const message = turn.event.status === "complete" ? "No response" : "Thinking…"; return new Text(theme.fg("userMessageText", message), this.paddingX, 0).render(width); }
private renderHint(width: number): string[] { // Any running turn blocks follow-ups (a completed notice can be the last // turn while an earlier question still streams), so check them all. const running = this.entries.some((entry) => entry.kind === "bash" ? entry.running : entry.event.status === "running", ); const hint = running ? "esc to cancel and return to session" : "reply to follow up · esc to return to session"; return new Text(theme.fg("dim", hint), this.paddingX, 0).render(width); }
private applySurface(line: string, width: number): string { const padded = line + " ".repeat(Math.max(0, width - visibleWidth(line))); const background = theme.getPopupBackgroundColor(); return padded .split("\x1b[0m") .map((segment) => background(segment)) .join("\x1b[0m"); }}