fix: print the running tool once, not twice (#17)
This commit was merged in pull request #17.
This commit is contained in:
@@ -116,6 +116,64 @@ describe("buildThreadMarkdown", () => {
|
||||
expect(markdown).not.toContain("Thinking");
|
||||
});
|
||||
|
||||
it("says what a running tool is doing exactly once, before any text arrives", () => {
|
||||
// The reported bug, from a screenshot: the activity line and the answer
|
||||
// placeholder both printed "Reading a page", one directly above the other.
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({
|
||||
role: "assistant",
|
||||
isStreaming: true,
|
||||
text: "",
|
||||
activity: { label: "Reading a page", count: 1 },
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(markdown.match(/Reading a page/g)).toHaveLength(1);
|
||||
// With nothing to read yet, the activity line is the stand-in, so the cursor
|
||||
// rides on it rather than sitting on a line of its own.
|
||||
expect(markdown).toContain(`_Reading a page…_ ${STREAMING_CURSOR}`);
|
||||
});
|
||||
|
||||
it("keeps the activity line once the answer has started arriving", () => {
|
||||
// The other half of the fix: a tool that starts mid-answer has to stay on
|
||||
// screen, which is why the line is not just a stand-in for missing text.
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({
|
||||
role: "assistant",
|
||||
isStreaming: true,
|
||||
text: "Here is what I found.",
|
||||
activity: { label: "Reading a page", count: 1 },
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(markdown).toContain("_Reading a page…_");
|
||||
expect(markdown).toContain("Here is what I found.");
|
||||
// And the cursor follows the answer, not the activity line.
|
||||
expect(markdown).toContain(`Here is what I found. ${STREAMING_CURSOR}`);
|
||||
expect(markdown).not.toContain(`_Reading a page…_ ${STREAMING_CURSOR}`);
|
||||
});
|
||||
|
||||
it("names a repeat rather than leaving a bare count to be interpreted", () => {
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({
|
||||
role: "assistant",
|
||||
isStreaming: true,
|
||||
activity: { label: "Searching the web", count: 2 },
|
||||
}),
|
||||
]);
|
||||
|
||||
expect(markdown).toContain("the same call again (2)");
|
||||
});
|
||||
|
||||
it("drops the activity line when the message is no longer streaming", () => {
|
||||
const markdown = buildThreadMarkdown([
|
||||
message({ role: "assistant", text: "done", activity: { label: "Searching the web", count: 1 } }),
|
||||
]);
|
||||
|
||||
expect(markdown).not.toContain("Searching the web");
|
||||
expect(markdown).not.toContain(STREAMING_CURSOR);
|
||||
});
|
||||
|
||||
it("shows a placeholder while an answer has no text yet", () => {
|
||||
expect(buildThreadMarkdown([message({ role: "assistant", isStreaming: true })])).toContain("Thinking");
|
||||
expect(buildThreadMarkdown([message({ role: "assistant" })])).toContain("No response");
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { buildThread, type LiveState } from "../lib/thread";
|
||||
import { buildThread, describeActivity, type LiveState } from "../lib/thread";
|
||||
import type { ChatTurn } from "../lib/types";
|
||||
|
||||
const idle: LiveState = { isStreaming: false, response: "", thinking: "" };
|
||||
|
||||
const activity = { label: "Reading a page", count: 1 };
|
||||
|
||||
function turn(id: string, prompt: string, response: string, overrides: Partial<ChatTurn> = {}): ChatTurn {
|
||||
return {
|
||||
id,
|
||||
@@ -133,3 +135,46 @@ describe("buildThread", () => {
|
||||
expect(thread[0].error).toBe("Request failed");
|
||||
});
|
||||
});
|
||||
|
||||
describe("carrying the running tool", () => {
|
||||
const live: LiveState = { isStreaming: true, response: "", thinking: "", activity };
|
||||
|
||||
it("puts it on the message being streamed", () => {
|
||||
const thread = buildThread([turn("t1", "q", "a")], live);
|
||||
const assistant = thread.find((message) => message.role === "assistant");
|
||||
|
||||
expect(assistant?.activity).toEqual(activity);
|
||||
});
|
||||
|
||||
it("keeps it off every earlier message", () => {
|
||||
// Only the newest turn can be mid-flight, and an older answer carrying a
|
||||
// "Searching the web" line would be a claim about a finished turn.
|
||||
const thread = buildThread([turn("t1", "q", "a"), turn("t2", "q2", "a2")], live);
|
||||
|
||||
expect(thread.slice(2).every((message) => message.activity === undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it("drops it once the turn is over", () => {
|
||||
const thread = buildThread([turn("t1", "q", "a")], { ...idle, activity });
|
||||
|
||||
expect(thread.every((message) => message.activity === undefined)).toBe(true);
|
||||
});
|
||||
|
||||
it("never puts one on a question", () => {
|
||||
const thread = buildThread([turn("t1", "q", "a")], live);
|
||||
|
||||
expect(thread.find((message) => message.role === "user")?.activity).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("describeActivity", () => {
|
||||
it("is just the label for a first call", () => {
|
||||
expect(describeActivity({ label: "Reading a page", count: 1 })).toBe("Reading a page");
|
||||
});
|
||||
|
||||
it("says a repeat is a repeat, because a bare number reads as progress", () => {
|
||||
expect(describeActivity({ label: "Reading a page", count: 3 })).toBe(
|
||||
"Reading a page · the same call again (3)",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user