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
+117
View File
@@ -0,0 +1,117 @@
import { describe, expect, it } from "vitest";
import {
STREAMING_CURSOR,
buildTurnMarkdown,
pendingMarkdown,
previewText,
sessionToPlainText,
} from "../lib/markdown";
import type { ChatTurn } from "../lib/types";
describe("buildTurnMarkdown", () => {
it("renders the question as a quote block above the answer", () => {
const markdown = buildTurnMarkdown({ prompt: "What is 2+2?", response: "4" });
expect(markdown).toContain("> What is 2+2?");
expect(markdown.indexOf("> What is 2+2?")).toBeLessThan(markdown.indexOf("4"));
});
it("keeps a multi-line question inside the quote block", () => {
const markdown = buildTurnMarkdown({ prompt: "line one\nline two", response: "ok" });
expect(markdown).toContain("> line one\n> line two");
});
it("marks a streaming answer with a cursor", () => {
const streaming = buildTurnMarkdown({ prompt: "q", response: "partial", isStreaming: true });
const finished = buildTurnMarkdown({ prompt: "q", response: "partial", isStreaming: false });
expect(streaming).toContain(STREAMING_CURSOR);
expect(finished).not.toContain(STREAMING_CURSOR);
});
it("shows a placeholder while an answer is still empty", () => {
expect(buildTurnMarkdown({ prompt: "q", response: "", isStreaming: true })).toContain("Thinking");
expect(buildTurnMarkdown({ prompt: "q", response: "", isStreaming: false })).toContain("No response");
});
it("surfaces an error instead of hiding it", () => {
const markdown = buildTurnMarkdown({ prompt: "q", response: "", error: "Ollama rejected the API key." });
expect(markdown).toContain("Ollama rejected the API key.");
});
it("lists tools that were used, flagging failures", () => {
const markdown = buildTurnMarkdown({
prompt: "q",
response: "a",
toolCalls: [
{ name: "web_search", arguments: {}, result: "ok", isError: false },
{ name: "web_fetch", arguments: {}, result: "boom", isError: true },
],
});
expect(markdown).toContain("`web_search`");
expect(markdown).toContain("`web_fetch` — failed");
});
it("omits the reasoning section when there is none", () => {
expect(buildTurnMarkdown({ prompt: "q", response: "a" })).not.toContain("Reasoning");
expect(buildTurnMarkdown({ prompt: "q", response: "a", thinking: "hmm" })).toContain("Reasoning");
});
it("includes the model and timestamp in the footer", () => {
const markdown = buildTurnMarkdown({
prompt: "q",
response: "a",
model: "gpt-oss:120b",
createdAt: 1_700_000_000_000,
});
expect(markdown).toContain("`gpt-oss:120b`");
});
});
describe("pendingMarkdown", () => {
it("shows the question with an activity line", () => {
const markdown = pendingMarkdown("q", "Searching the web");
expect(markdown).toContain("> q");
expect(markdown).toContain("_Searching the web…_");
});
it("falls back to a generic line when there is no status", () => {
expect(pendingMarkdown("q")).toContain("_Thinking…_");
});
});
describe("previewText", () => {
it("collapses newlines so a preview stays on one row", () => {
expect(previewText("a\n\nb", 40)).toBe("a b");
});
it("clips with an ellipsis", () => {
expect(previewText("a".repeat(50), 5)).toHaveLength(5);
});
});
describe("sessionToPlainText", () => {
function turn(prompt: string, response: string, error?: string): ChatTurn {
return { id: prompt, prompt, response, thinking: "", model: "m", createdAt: 0, toolCalls: [], error };
}
it("returns an empty string for an empty conversation", () => {
expect(sessionToPlainText([])).toBe("");
});
it("keeps every question and answer", () => {
const text = sessionToPlainText([turn("q1", "a1"), turn("q2", "a2")]);
expect(text).toContain("## q1");
expect(text).toContain("a1");
expect(text).toContain("## q2");
expect(text).toContain("a2");
});
it("includes the failure when a turn errored", () => {
const text = sessionToPlainText([turn("q", "", "Ollama rejected the API key.")]);
expect(text).toContain("Ollama rejected the API key.");
});
});