File size: 7,410 Bytes
39371ea | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | import { Container, Markdown, MouseRegion, Spacer, Text } from "@earendil-works/pi-tui";
import { getMarkdownTheme, theme } from "../theme/theme.js";
import { createMarkdownTransform } from "./markdown-transform.js";
const OSC133_ZONE_START = "\x1b]133;A\x07";
const OSC133_ZONE_END = "\x1b]133;B\x07";
const OSC133_ZONE_FINAL = "\x1b]133;C\x07";
/**
* Component that renders a complete assistant message
*/
export class AssistantMessageComponent extends Container {
contentContainer;
hideThinkingBlock;
markdownTheme;
hiddenThinkingLabel;
outputPad;
markdownTransformers;
lastMessage;
hasToolCalls = false;
isStreaming = false;
thinkingVisibilityOverrides = new Map();
constructor(message, hideThinkingBlock = false, markdownTheme = getMarkdownTheme(), hiddenThinkingLabel = "Thinking...", outputPad = 1, markdownTransformers = []) {
super();
this.hideThinkingBlock = hideThinkingBlock;
this.markdownTheme = markdownTheme;
this.hiddenThinkingLabel = hiddenThinkingLabel;
this.outputPad = outputPad;
this.markdownTransformers = markdownTransformers;
// Container for text/thinking content
this.contentContainer = new Container();
this.addChild(this.contentContainer);
if (message) {
this.updateContent(message);
}
}
invalidate() {
super.invalidate();
if (this.lastMessage) {
this.updateContent(this.lastMessage);
}
}
setHideThinkingBlock(hide) {
this.hideThinkingBlock = hide;
this.thinkingVisibilityOverrides.clear();
if (this.lastMessage) {
this.updateContent(this.lastMessage);
}
}
setHiddenThinkingLabel(label) {
this.hiddenThinkingLabel = label;
if (this.lastMessage) {
this.updateContent(this.lastMessage);
}
}
setOutputPad(padding) {
this.outputPad = padding;
if (this.lastMessage) {
this.updateContent(this.lastMessage);
}
}
render(width) {
const lines = super.render(width);
if (this.hasToolCalls || lines.length === 0) {
return lines;
}
lines[0] = OSC133_ZONE_START + lines[0];
lines[lines.length - 1] = OSC133_ZONE_END + OSC133_ZONE_FINAL + lines[lines.length - 1];
return lines;
}
updateContent(message, isStreaming = this.isStreaming) {
this.lastMessage = message;
this.isStreaming = isStreaming;
// Clear content container
this.contentContainer.clear();
const hasVisibleContent = message.content.some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
if (hasVisibleContent) {
this.contentContainer.addChild(new Spacer(1));
}
// Render content in order
let thinkingRunIndex = 0;
for (let i = 0; i < message.content.length; i++) {
const content = message.content[i];
if (content.type === "text" && content.text.trim()) {
// Assistant text messages with no background - trim the text
// Set paddingY=0 to avoid extra spacing before tool executions
this.contentContainer.addChild(new Markdown(content.text.trim(), this.outputPad, 0, this.markdownTheme, undefined, {
transform: createMarkdownTransform("assistant", this.isStreaming, this.markdownTransformers),
}));
}
else if (content.type === "thinking") {
const thinkingBlocks = [];
for (; i < message.content.length; i++) {
const thinkingContent = message.content[i];
if (thinkingContent.type !== "thinking") {
break;
}
const thinking = thinkingContent.thinking.trim();
if (thinking) {
thinkingBlocks.push(thinking);
}
}
i--;
if (thinkingBlocks.length === 0) {
continue;
}
// Add spacing only when another visible assistant content block follows.
// This avoids a superfluous blank line before separately-rendered tool execution blocks.
const hasVisibleContentAfter = message.content
.slice(i + 1)
.some((c) => (c.type === "text" && c.text.trim()) || (c.type === "thinking" && c.thinking.trim()));
const runIndex = thinkingRunIndex++;
const hidden = this.thinkingVisibilityOverrides.get(runIndex) ?? this.hideThinkingBlock;
const thinkingComponent = hidden
? new Text(theme.italic(theme.fg("thinkingText", this.hiddenThinkingLabel)), this.outputPad, 0)
: new Markdown(thinkingBlocks.join("\n\n"), this.outputPad, 0, this.markdownTheme, {
color: (text) => theme.fg("thinkingText", text),
italic: true,
}, {
transform: createMarkdownTransform("assistant-thinking", this.isStreaming, this.markdownTransformers),
});
this.contentContainer.addChild(new MouseRegion(thinkingComponent, (event) => {
if (event.type !== "click" || event.button !== "left")
return undefined;
this.thinkingVisibilityOverrides.set(runIndex, !hidden);
if (this.lastMessage)
this.updateContent(this.lastMessage);
return { handled: true };
}));
if (hasVisibleContentAfter) {
this.contentContainer.addChild(new Spacer(1));
}
}
}
// Check if incomplete/failed - show after partial content.
// For aborted/error tool calls, tool execution components show the error.
// Length stops can happen before a tool call is complete, so surface them here too.
const hasToolCalls = message.content.some((c) => c.type === "toolCall");
this.hasToolCalls = hasToolCalls;
if (message.stopReason === "length") {
this.contentContainer.addChild(new Spacer(1));
this.contentContainer.addChild(new Text(theme.fg("error", "Response was truncated before completion."), this.outputPad, 0));
}
else if (!hasToolCalls) {
if (message.stopReason === "aborted") {
const abortMessage = message.errorMessage && message.errorMessage !== "Request was aborted"
? message.errorMessage
: "Operation aborted";
this.contentContainer.addChild(new Spacer(1));
this.contentContainer.addChild(new Text(theme.fg("error", abortMessage), this.outputPad, 0));
}
else if (message.stopReason === "error") {
const errorMsg = message.errorMessage || "Unknown error";
this.contentContainer.addChild(new Spacer(1));
this.contentContainer.addChild(new Text(theme.fg("error", `Error: ${errorMsg}`), this.outputPad, 0));
}
}
}
}
//# sourceMappingURL=assistant-message.js.map |