91 lines
3.1 KiB
TypeScript
91 lines
3.1 KiB
TypeScript
import type { ChatTurn, ThreadMessage } from "./types";
|
|
|
|
/**
|
|
* Markdown rendering for the detail panel.
|
|
*
|
|
* Kept as pure functions so the transcript layout can be tested without
|
|
* mounting a component.
|
|
*
|
|
* The user/assistant distinction is carried by markdown itself: user messages
|
|
* render as blockquotes, which the renderer draws in a dimmed italic style,
|
|
* while answers render as normal body text. That mirrors how the list rows
|
|
* dim the question and brighten the answer, so the two panes agree.
|
|
*/
|
|
|
|
export const STREAMING_CURSOR = "▍";
|
|
|
|
/** Prefixes every line so a multi-line message stays one continuous quote block. */
|
|
export function quote(text: string): string {
|
|
return text
|
|
.split("\n")
|
|
.map((line) => `> ${line}`.trimEnd())
|
|
.join("\n");
|
|
}
|
|
|
|
/** A short one-line preview of a message, for a list row. */
|
|
export function previewText(text: string, max: number): string {
|
|
const collapsed = text.replace(/\s+/g, " ").trim();
|
|
if (collapsed.length <= max) return collapsed;
|
|
return `${collapsed.slice(0, Math.max(0, max - 1)).trimEnd()}…`;
|
|
}
|
|
|
|
function renderMessage(message: ThreadMessage): string {
|
|
if (message.role === "user") {
|
|
return `**You**\n\n${quote(message.text)}`;
|
|
}
|
|
|
|
const body = message.text.trim() || (message.isStreaming ? "_Thinking…_" : "_No response._");
|
|
const sections = ["**Lam**", "", message.isStreaming ? `${body} ${STREAMING_CURSOR}` : body];
|
|
|
|
if (message.error) {
|
|
sections.push("", `**Error** — ${message.error}`);
|
|
}
|
|
|
|
if (message.toolCalls.length > 0) {
|
|
const lines = message.toolCalls.map(
|
|
(call) => `- \`${call.name}\`${call.isError ? " — failed" : ""}`,
|
|
);
|
|
sections.push("", "**Tools used**", ...lines);
|
|
}
|
|
|
|
if (message.thinking.trim().length > 0) {
|
|
sections.push("", "_Reasoning_", "", quote(message.thinking.trim()));
|
|
}
|
|
|
|
return sections.join("\n");
|
|
}
|
|
|
|
/**
|
|
* Renders the whole conversation.
|
|
*
|
|
* The detail panel shows the full thread rather than just the selected message
|
|
* on purpose. Vicinae's list resets its selection to the first row whenever the
|
|
* model changes, and streaming changes it on every flush, so the selected row
|
|
* is not something the extension can rely on staying put while an answer
|
|
* arrives. Showing the thread means the live answer is always on screen.
|
|
*/
|
|
export function buildThreadMarkdown(messages: ThreadMessage[]): string {
|
|
if (messages.length === 0) return "";
|
|
return messages.map(renderMessage).join("\n\n---\n\n");
|
|
}
|
|
|
|
/**
|
|
* Flattens a whole conversation into plain text for the clipboard.
|
|
*
|
|
* This has to load the full transcript rather than reuse the index summary —
|
|
* copying a truncated preview and calling it "the conversation" would be a
|
|
* quiet lie.
|
|
*/
|
|
export function sessionToPlainText(turns: ChatTurn[]): string {
|
|
if (turns.length === 0) return "";
|
|
|
|
return turns
|
|
.map((turn) => {
|
|
const header = `## ${turn.prompt}`;
|
|
const body = turn.response.trim() || (turn.error ? "" : "_(no response)_");
|
|
const failure = turn.error ? `\n\n**[error]** ${turn.error}` : "";
|
|
return `${header}\n\n${body}${failure}`;
|
|
})
|
|
.join("\n\n---\n\n");
|
|
}
|