/** * Assistant-message dot transformer (pure, no pi imports — unit-testable). * * Prepend a "● " to assistant text messages, mirroring Claude Code. Thinking * blocks and user messages pass through untouched, as does any message that * opens with a code fence (a "● " before "```" would break code-block * parsing). * * Streaming: partial updates re-run this transform every frame with the raw * markdown as input, so the prefix never accumulates. A fence can arrive one * backtick at a time ("`", "``", "```js") — while isStreaming is true, any * leading backtick is treated as a fence start to avoid a "● `" flicker on * intermediate frames. The final frame keeps the full ``` check, so replies * that merely open with inline code (e.g. "`foo` and `bar`") still get the * dot. */ export const ASSISTANT_DOT = "\u2B24 "; export interface AssistantDotContext { /** "user" | "assistant" | "assistant-thinking". */ messageType?: string; /** True for partial assistant updates. */ isStreaming?: boolean; } export function assistantDotTransformer( markdown: string, { messageType, isStreaming }: AssistantDotContext = {}, ): string { // Only assistant text gets the dot; user messages and thinking blocks // (assistant-thinking) pass through untouched. if (messageType !== "assistant") return markdown; const trimmed = markdown.trimStart(); if (!trimmed) return markdown; // Idempotent: never stack a second dot if the text already starts with one // (e.g. a previous transformer output fed back in). if (trimmed.startsWith(ASSISTANT_DOT)) return markdown; const fenceStart = isStreaming ? trimmed.startsWith("`") : trimmed.startsWith("```"); if (fenceStart) return markdown; return ASSISTANT_DOT + trimmed; }