fix(webui): finalize turn-ending slash commands
maintainer edit: keep /new and manually submitted /stop from leaving stale WebUI streaming state after they cancel or reset the active turn.
This commit is contained in:
@@ -1345,6 +1345,47 @@ describe("ThreadComposer", () => {
|
||||
expect(onSend).toHaveBeenCalledWith("/status", undefined, { sideChannel: true });
|
||||
});
|
||||
|
||||
it("marks new chat commands as side-channel sends that finalize the active turn", () => {
|
||||
const onSend = vi.fn();
|
||||
render(
|
||||
<ThreadComposer
|
||||
onSend={onSend}
|
||||
placeholder="Type your message..."
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("Message input");
|
||||
fireEvent.change(input, { target: { value: "/new" } });
|
||||
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||
|
||||
expect(onSend).toHaveBeenCalledWith(
|
||||
"/new",
|
||||
undefined,
|
||||
{ sideChannel: true, finalizeActiveTurn: true },
|
||||
);
|
||||
});
|
||||
|
||||
it("routes a manually submitted stop command through the stop handler", () => {
|
||||
const onSend = vi.fn();
|
||||
const onStop = vi.fn();
|
||||
render(
|
||||
<ThreadComposer
|
||||
onSend={onSend}
|
||||
onStop={onStop}
|
||||
isStreaming
|
||||
placeholder="Type your message..."
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("Message input");
|
||||
fireEvent.change(input, { target: { value: "/stop" } });
|
||||
fireEvent.keyDown(input, { key: "Escape" });
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
|
||||
expect(onStop).toHaveBeenCalledTimes(1);
|
||||
expect(onSend).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("keeps goal task commands on the normal agent turn path", () => {
|
||||
const onSend = vi.fn();
|
||||
render(
|
||||
|
||||
@@ -1653,6 +1653,63 @@ describe("useNanobotStream", () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it("finalizes active streaming before turn-ending side-channel commands", async () => {
|
||||
const fake = fakeClient();
|
||||
const { result } = renderHook(() => useNanobotStream("chat-new", EMPTY_MESSAGES), {
|
||||
wrapper: wrap(fake.client),
|
||||
});
|
||||
|
||||
act(() => {
|
||||
result.current.send("long task");
|
||||
});
|
||||
const activeTurnId = fake.client.sendMessage.mock.calls.at(-1)![3]?.turnId;
|
||||
|
||||
act(() => {
|
||||
fake.emit("chat-new", {
|
||||
event: "delta",
|
||||
chat_id: "chat-new",
|
||||
text: "partial answer",
|
||||
turn_id: activeTurnId,
|
||||
});
|
||||
});
|
||||
await flushStreamFrame();
|
||||
|
||||
expect(result.current.isStreaming).toBe(true);
|
||||
expect(result.current.messages.find((message) => message.content === "partial answer"))
|
||||
.toMatchObject({ isStreaming: true });
|
||||
|
||||
act(() => {
|
||||
result.current.send("/new", undefined, {
|
||||
sideChannel: true,
|
||||
finalizeActiveTurn: true,
|
||||
});
|
||||
});
|
||||
|
||||
const newCall = fake.client.sendMessage.mock.calls.at(-1)!;
|
||||
expect(newCall[3]).not.toHaveProperty("sideChannel");
|
||||
expect(newCall[3]).not.toHaveProperty("finalizeActiveTurn");
|
||||
expect(result.current.isStreaming).toBe(false);
|
||||
expect(result.current.messages.find((message) => message.content === "partial answer"))
|
||||
.toMatchObject({ isStreaming: false });
|
||||
|
||||
act(() => {
|
||||
fake.emit("chat-new", {
|
||||
event: "message",
|
||||
chat_id: "chat-new",
|
||||
text: "New session started.",
|
||||
turn_id: newCall[3]?.turnId,
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.isStreaming).toBe(false);
|
||||
expect(result.current.messages.map((message) => message.content)).toEqual([
|
||||
"long task",
|
||||
"partial answer",
|
||||
"/new",
|
||||
"New session started.",
|
||||
]);
|
||||
});
|
||||
|
||||
it("lets stream_end finish streaming while side-channel status replies arrive", () => {
|
||||
vi.useFakeTimers();
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user