feat: strip speaker labels from the transcript, add a plain-prose prompt (#9)

This commit was merged in pull request #9.
This commit is contained in:
2026-10-03 21:28:38 +00:00
parent ee96f1ee61
commit c49ffeb274
4 changed files with 92 additions and 67 deletions
+41 -23
View File
@@ -1,8 +1,6 @@
import { describe, expect, it } from "vitest";
import {
ASSISTANT_LABEL,
STREAMING_CURSOR,
USER_LABEL,
buildThreadMarkdown,
quote,
sessionToPlainText,
@@ -38,12 +36,29 @@ describe("buildThreadMarkdown", () => {
message({ role: "assistant", text: "4" }),
]);
expect(markdown).toContain("**You**");
expect(markdown).toContain("> What is 2+2?");
expect(markdown).toContain("**Lam**");
expect(markdown).toContain("4");
});
it("labels no speaker, so only the answer carries weight", () => {
// A `**You**` / `**Lam**` header on each exchange gave both speakers the
// same visual weight and pushed the answer behind chrome.
const markdown = buildThreadMarkdown([
message({ role: "user", text: "q" }),
message({ role: "assistant", text: "a" }),
]);
expect(markdown).not.toContain("You");
expect(markdown).not.toContain("Lam");
// Nothing bold at all, other than an error.
expect(markdown).not.toContain("**");
});
it("identifies the question by styling alone", () => {
const markdown = buildThreadMarkdown([message({ role: "user", text: "just a question" })]);
expect(markdown.trim()).toBe("> just a question");
});
it("groups an answer with its question, dividing only between exchanges", () => {
const markdown = buildThreadMarkdown([
message({ id: "t2:assistant", role: "assistant", text: "second answer" }),
@@ -59,17 +74,6 @@ describe("buildThreadMarkdown", () => {
expect(markdown.indexOf("second question")).toBeLessThan(markdown.indexOf("first answer"));
});
it("labels each speaker, in order", () => {
const markdown = buildThreadMarkdown([
message({ role: "user", text: "q" }),
message({ role: "assistant", text: "a" }),
]);
expect(markdown).toContain(`**${USER_LABEL}**`);
expect(markdown).toContain(`**${ASSISTANT_LABEL}**`);
expect(markdown.indexOf(USER_LABEL)).toBeLessThan(markdown.indexOf(ASSISTANT_LABEL));
});
it("renders no images, because markdown cannot resolve them", () => {
// Images render block-level and centred at up to 200px (`MdImage.qml`), and
// every candidate icon source fails. Keep the transcript text-only.
@@ -123,7 +127,7 @@ describe("buildThreadMarkdown", () => {
expect(markdown).toContain("Ollama rejected the API key.");
});
it("lists tools that were used, flagging failures", () => {
it("lists tools on one quiet line, flagging failures", () => {
const markdown = buildThreadMarkdown([
message({
role: "assistant",
@@ -135,8 +139,9 @@ describe("buildThreadMarkdown", () => {
}),
]);
expect(markdown).toContain("`web_search`");
expect(markdown).toContain("`web_fetch` — failed");
expect(markdown).toContain("_Tools: `web_search`, `web_fetch` (failed)_");
// Italic, not a bold header, so it stays below the answer.
expect(markdown).not.toContain("**Tools");
});
it("omits tools and reasoning when the preferences are off", () => {
@@ -151,7 +156,7 @@ describe("buildThreadMarkdown", () => {
const hidden = buildThreadMarkdown(messages, { showToolCalls: false, showReasoning: false });
expect(hidden).not.toContain("Tools used");
expect(hidden).not.toContain("_Tools:");
expect(hidden).not.toContain("web_search");
expect(hidden).not.toContain("Reasoning");
expect(hidden).not.toContain("let me think");
@@ -170,11 +175,11 @@ describe("buildThreadMarkdown", () => {
];
const toolsOnly = buildThreadMarkdown(messages, { showToolCalls: true, showReasoning: false });
expect(toolsOnly).toContain("Tools used");
expect(toolsOnly).toContain("_Tools:");
expect(toolsOnly).not.toContain("Reasoning");
const reasoningOnly = buildThreadMarkdown(messages, { showToolCalls: false, showReasoning: true });
expect(reasoningOnly).not.toContain("Tools used");
expect(reasoningOnly).not.toContain("_Tools:");
expect(reasoningOnly).toContain("Reasoning");
});
@@ -198,12 +203,25 @@ describe("sessionToPlainText", () => {
it("keeps every question and answer", () => {
const text = sessionToPlainText([turn("q1", "a1"), turn("q2", "a2")]);
expect(text).toContain("## q1");
expect(text).toContain("q1");
expect(text).toContain("a1");
expect(text).toContain("## q2");
expect(text).toContain("q2");
expect(text).toContain("a2");
});
it("labels the speakers, because the transcript's styling does not survive a clipboard", () => {
const text = sessionToPlainText([turn("q1", "a1")]);
expect(text).toContain("**You**");
expect(text).toContain("**Lam**");
expect(text.indexOf("q1")).toBeLessThan(text.indexOf("a1"));
});
it("reads oldest first, unlike the transcript", () => {
const text = sessionToPlainText([turn("first", "a1"), turn("second", "a2")]);
expect(text.indexOf("first")).toBeLessThan(text.indexOf("second"));
});
it("includes the failure when a turn errored", () => {
expect(sessionToPlainText([turn("q", "", "Ollama rejected the API key.")])).toContain(
"Ollama rejected the API key.",