import type { Component } from "@earendil-works/pi-tui"; import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui"; import type { Theme } from "@earendil-works/pi-coding-agent"; import type { SessionInfo, Message } from "../types.ts"; import { formatSessionDisplayName, sanitizeDisplayText } from "./session-identity.ts"; import { formatMessageTiming } from "./timestamps.ts"; export class InlineMessageComponent implements Component { private from: SessionInfo; private message: Message; private theme: Theme; private replyCommand?: string; private bodyText?: string; private collapsed: boolean; private receivedAt?: number; private readAt?: number; constructor( from: SessionInfo, message: Message, theme: Theme, replyCommand?: string, bodyText?: string, collapsed = false, receivedAt?: number, readAt?: number, ) { this.from = from; this.message = message; this.theme = theme; this.replyCommand = replyCommand; this.bodyText = bodyText; this.collapsed = collapsed; this.receivedAt = receivedAt; this.readAt = readAt; } invalidate(): void {} render(width: number): string[] { const lines: string[] = []; const borderChar = "โ”€"; const senderName = formatSessionDisplayName(this.from, "Unknown sender"); const senderCwd = sanitizeDisplayText(this.from.cwd, "Unknown path"); if (width < 3) { return [truncateToWidth(`From ${senderName}`, width)]; } const bodyWidth = Math.max(1, width - 2); const header = ` ๐Ÿ“จ From: ${senderName} (${senderCwd}) `; const headerText = truncateToWidth(this.collapsed ? `${header} Ctrl+O expands ` : header, bodyWidth, ""); const headerPadding = Math.max(0, bodyWidth - visibleWidth(headerText)); lines.push(this.theme.fg("accent", `โ•ญ${headerText}${borderChar.repeat(headerPadding)}โ•ฎ`)); if (this.collapsed) { const preview = (this.bodyText || this.message.content.text).replace(/\s+/g, " ").trim(); const previewText = truncateToWidth(preview, bodyWidth, ""); const previewPadding = Math.max(0, bodyWidth - visibleWidth(previewText)); lines.push(this.theme.fg("accent", `โ”‚${previewText}${" ".repeat(previewPadding)}โ”‚`)); const meta: string[] = []; const timing = formatMessageTiming({ sentAt: this.message.timestamp, receivedAt: this.receivedAt, readAt: this.readAt }); if (timing) meta.push(timing); if (this.replyCommand) meta.push(`โ†ฉ To reply: ${this.replyCommand}`); if (this.message.content.attachments?.length) { const count = this.message.content.attachments.length; meta.push(`๐Ÿ“Ž ${count} attachment${count === 1 ? "" : "s"}`); } if (this.message.replyTo && !this.message.expectsReply) meta.push(`โ†ณ Reply to ${this.message.replyTo.slice(0, 8)}`); meta.push("Ctrl+O to expand"); const metaText = truncateToWidth(this.theme.fg("dim", ` ${meta.join(" ยท ")}`), bodyWidth, ""); const metaPadding = Math.max(0, bodyWidth - visibleWidth(metaText)); lines.push(this.theme.fg("accent", `โ”‚${metaText}${" ".repeat(metaPadding)}โ”‚`)); lines.push(this.theme.fg("accent", `โ•ฐ${borderChar.repeat(bodyWidth)}โ•ฏ`)); return lines; } const contentLines = wrapTextWithAnsi(this.bodyText || this.message.content.text, bodyWidth); for (const line of contentLines) { const text = truncateToWidth(line, bodyWidth, ""); const padding = Math.max(0, bodyWidth - visibleWidth(text)); lines.push(this.theme.fg("accent", `โ”‚${text}${" ".repeat(padding)}โ”‚`)); } if (this.replyCommand) { lines.push(this.theme.fg("accent", `โ”‚${" ".repeat(bodyWidth)}โ”‚`)); const replyLines = wrapTextWithAnsi(this.theme.fg("dim", ` โ†ฉ To reply: ${this.replyCommand}`), bodyWidth); for (const line of replyLines) { const text = truncateToWidth(line, bodyWidth, ""); const padding = Math.max(0, bodyWidth - visibleWidth(text)); lines.push(this.theme.fg("accent", `โ”‚${text}${" ".repeat(padding)}โ”‚`)); } } if (this.message.content.attachments?.length) { lines.push(this.theme.fg("accent", `โ”‚${" ".repeat(bodyWidth)}โ”‚`)); for (const att of this.message.content.attachments) { const label = this.theme.fg("dim", ` ๐Ÿ“Ž ${sanitizeDisplayText(att.name, "Unnamed attachment")}`); const text = truncateToWidth(label, bodyWidth, ""); const padding = Math.max(0, bodyWidth - visibleWidth(text)); lines.push(this.theme.fg("accent", `โ”‚${text}${" ".repeat(padding)}โ”‚`)); } } if (this.message.replyTo && !this.message.expectsReply) { lines.push(this.theme.fg("accent", `โ”‚${" ".repeat(bodyWidth)}โ”‚`)); const reply = this.theme.fg("dim", ` โ†ณ Reply to ${this.message.replyTo.slice(0, 8)}`); const text = truncateToWidth(reply, bodyWidth, ""); const padding = Math.max(0, bodyWidth - visibleWidth(text)); lines.push(this.theme.fg("accent", `โ”‚${text}${" ".repeat(padding)}โ”‚`)); } const timing = formatMessageTiming({ sentAt: this.message.timestamp, receivedAt: this.receivedAt, readAt: this.readAt }); if (timing) { lines.push(this.theme.fg("accent", `โ”‚${" ".repeat(bodyWidth)}โ”‚`)); const timingText = truncateToWidth(this.theme.fg("dim", ` ${timing}`), bodyWidth, ""); const timingPadding = Math.max(0, bodyWidth - visibleWidth(timingText)); lines.push(this.theme.fg("accent", `โ”‚${timingText}${" ".repeat(timingPadding)}โ”‚`)); } lines.push(this.theme.fg("accent", `โ•ฐ${borderChar.repeat(bodyWidth)}โ•ฏ`)); return lines; } }