feat: initial Lam Agent extension (#1)

This commit was merged in pull request #1.
This commit is contained in:
2026-10-03 20:32:40 +00:00
parent 1d39aabb50
commit 8b507e7953
36 changed files with 7136 additions and 2 deletions
+98
View File
@@ -0,0 +1,98 @@
import type { ChatTurn } from "./types";
/**
* Renders a turn as markdown for the detail panel.
*
* Kept as a pure function so the transcript layout can be tested without
* mounting a component, and so the same renderer can serve both a finished
* turn and one that is still streaming.
*/
export const STREAMING_CURSOR = "▍";
export interface TurnMarkdownInput {
prompt: string;
response: string;
thinking?: string;
toolCalls?: ChatTurn["toolCalls"];
error?: string;
model?: string;
createdAt?: number;
isStreaming?: boolean;
}
/** Prefixes every line so a multi-line prompt renders as one continuous quote block. */
function quote(text: string): string {
return text
.split("\n")
.map((line) => `> ${line}`.trimEnd())
.join("\n");
}
export function buildTurnMarkdown(input: TurnMarkdownInput): string {
const sections: string[] = [];
sections.push(quote(input.prompt));
sections.push("");
const body = input.response.trim() || (input.isStreaming ? "_Thinking…_" : "_No response._");
sections.push(input.isStreaming ? `${body} ${STREAMING_CURSOR}` : body);
if (input.error) {
sections.push("", `**Error** — ${input.error}`);
}
const toolCalls = input.toolCalls ?? [];
if (toolCalls.length > 0) {
const lines = toolCalls.map((call) => `- \`${call.name}\`${call.isError ? " — failed" : ""}`);
sections.push("", "**Tools used**", ...lines);
}
if (input.thinking && input.thinking.trim().length > 0) {
sections.push("", "---", "", "**Reasoning**", "", quote(input.thinking.trim()));
}
const footer: string[] = [];
if (input.model) footer.push(`\`${input.model}\``);
if (input.createdAt) footer.push(new Date(input.createdAt).toLocaleString());
if (footer.length > 0) {
sections.push("", "---", "", footer.join(" · "));
}
return sections.join("\n");
}
/** The placeholder shown while the first tokens are still in flight. */
export function pendingMarkdown(prompt: string, status?: string): string {
const lines = [quote(prompt), ""];
lines.push(status ? `_${status}…_` : "_Thinking…_");
return lines.join("\n");
}
/** A short one-line preview of an answer, for a collapsed 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()}…`;
}
/**
* 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");
}