fix(webui): hide actions until turn end
This commit is contained in:
@@ -1055,6 +1055,122 @@ describe("ThreadMessages", () => {
|
||||
expect(screen.getByText("final reply")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides current turn actions until turn_end", () => {
|
||||
const activeTurnId = "turn-2";
|
||||
const messages: UIMessage[] = [
|
||||
{ id: "u1", role: "user", content: "old question", turnId: "turn-1", createdAt: 1 },
|
||||
{ id: "a1", role: "assistant", content: "old answer", turnId: "turn-1", createdAt: 2 },
|
||||
{ id: "u2", role: "user", content: "new question", turnId: activeTurnId, createdAt: 3 },
|
||||
{
|
||||
id: "a2",
|
||||
role: "assistant",
|
||||
content: "first answer slice",
|
||||
turnId: activeTurnId,
|
||||
createdAt: 4,
|
||||
},
|
||||
{
|
||||
id: "t2",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: "search()",
|
||||
traces: ["search()"],
|
||||
turnId: activeTurnId,
|
||||
createdAt: 5,
|
||||
},
|
||||
{
|
||||
id: "a3",
|
||||
role: "assistant",
|
||||
content: "second answer slice",
|
||||
turnId: activeTurnId,
|
||||
createdAt: 6,
|
||||
},
|
||||
];
|
||||
const props = { messages, onForkFromMessage: vi.fn() };
|
||||
const { container, rerender } = render(
|
||||
<ThreadMessages {...props} isStreaming activeTurnId={activeTurnId} />,
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll('[data-assistant-footer] [aria-label="Copy"]')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('[data-assistant-footer] [aria-label="Fork"]')).toHaveLength(1);
|
||||
|
||||
rerender(<ThreadMessages {...props} isStreaming={false} activeTurnId={null} />);
|
||||
|
||||
expect(container.querySelectorAll('[data-assistant-footer] [aria-label="Copy"]')).toHaveLength(3);
|
||||
expect(container.querySelectorAll('[data-assistant-footer] [aria-label="Fork"]')).toHaveLength(2);
|
||||
});
|
||||
|
||||
it("keeps active turn actions hidden across guidance and failed user rows", () => {
|
||||
const activeTurnId = "turn-active";
|
||||
const messages: UIMessage[] = [
|
||||
{ id: "old-user", role: "user", content: "old question", turnId: "turn-old", createdAt: 1 },
|
||||
{ id: "old", role: "assistant", content: "old answer", turnId: "turn-old", createdAt: 2 },
|
||||
{ id: "active-user", role: "user", content: "new question", turnId: activeTurnId, createdAt: 3 },
|
||||
{ id: "live", role: "assistant", content: "live slice", createdAt: 4 },
|
||||
{ id: "guide", role: "user", content: "focus", turnId: "turn-guide", createdAt: 5 },
|
||||
{
|
||||
id: "failed",
|
||||
role: "user",
|
||||
content: "retry",
|
||||
turnId: "turn-failed",
|
||||
deliveryStatus: "failed",
|
||||
createdAt: 6,
|
||||
},
|
||||
];
|
||||
const { container } = render(
|
||||
<ThreadMessages
|
||||
messages={messages}
|
||||
isStreaming
|
||||
activeTurnId={activeTurnId}
|
||||
onForkFromMessage={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll('[data-assistant-footer] [aria-label="Copy"]')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('[data-assistant-footer] [aria-label="Fork"]')).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("only hides the active assistant-only automation turn", () => {
|
||||
const { container } = render(
|
||||
<ThreadMessages
|
||||
messages={[
|
||||
{ id: "old", role: "assistant", content: "old answer", turnId: "turn-old", createdAt: 1 },
|
||||
{
|
||||
id: "automation",
|
||||
role: "assistant",
|
||||
content: "automation result",
|
||||
turnId: "turn-automation",
|
||||
createdAt: 2,
|
||||
},
|
||||
]}
|
||||
isStreaming
|
||||
activeTurnId="turn-automation"
|
||||
onForkFromMessage={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelectorAll('[data-assistant-footer] [aria-label="Copy"]')).toHaveLength(1);
|
||||
expect(container.querySelectorAll('[data-assistant-footer] [aria-label="Fork"]')).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("falls back to the latest user boundary for untagged active slices", () => {
|
||||
const { container } = render(
|
||||
<ThreadMessages
|
||||
messages={[
|
||||
{ id: "user", role: "user", content: "question", createdAt: 1 },
|
||||
{ id: "live", role: "assistant", content: "live slice", createdAt: 2 },
|
||||
]}
|
||||
isStreaming
|
||||
activeTurnId="turn-active"
|
||||
onForkFromMessage={vi.fn()}
|
||||
/>,
|
||||
);
|
||||
|
||||
expect(container.querySelector('[data-assistant-footer] [aria-label="Copy"]'))
|
||||
.not.toBeInTheDocument();
|
||||
expect(container.querySelector('[data-assistant-footer] [aria-label="Fork"]'))
|
||||
.not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows copy on adjacent assistant text slices", () => {
|
||||
const messages: UIMessage[] = [
|
||||
{ id: "a1", role: "assistant", content: "part one", createdAt: 1 },
|
||||
|
||||
@@ -471,6 +471,54 @@ describe("ThreadShell", () => {
|
||||
expect(screen.queryByText("failed to read file")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("hides actions for a complete assistant-only message until turn_end", async () => {
|
||||
const client = makeClient();
|
||||
vi.mocked(fetch).mockImplementation(async (input) => (
|
||||
String(input).includes("websocket%3Aassistant-only-actions/webui-thread")
|
||||
? httpJson(transcriptFromSimpleMessages([
|
||||
{ role: "assistant", content: "old automation", turnId: "turn-old" },
|
||||
]))
|
||||
: { ok: false, status: 404, json: async () => ({}) }
|
||||
) as Response);
|
||||
|
||||
render(wrap(
|
||||
client,
|
||||
<ThreadShell
|
||||
session={session("assistant-only-actions")}
|
||||
title="Assistant-only actions"
|
||||
onToggleSidebar={() => {}}
|
||||
/>,
|
||||
));
|
||||
|
||||
await waitFor(() => expect(screen.getAllByRole("button", { name: "Copy" })).toHaveLength(1));
|
||||
const turnId = "turn-automation";
|
||||
const startedAt = Date.now() / 1000;
|
||||
act(() => client._emitChat("assistant-only-actions", {
|
||||
event: "goal_status",
|
||||
chat_id: "assistant-only-actions",
|
||||
status: "running",
|
||||
started_at: startedAt,
|
||||
turn_id: turnId,
|
||||
}));
|
||||
expect(screen.getAllByRole("button", { name: "Copy" })).toHaveLength(1);
|
||||
|
||||
act(() => client._emitChat("assistant-only-actions", {
|
||||
event: "message",
|
||||
chat_id: "assistant-only-actions",
|
||||
text: "new automation",
|
||||
turn_id: turnId,
|
||||
}));
|
||||
await waitFor(() => expect(screen.getByText("new automation")).toBeInTheDocument());
|
||||
expect(screen.getAllByRole("button", { name: "Copy" })).toHaveLength(1);
|
||||
|
||||
act(() => client._emitChat("assistant-only-actions", {
|
||||
event: "turn_end",
|
||||
chat_id: "assistant-only-actions",
|
||||
turn_id: turnId,
|
||||
}));
|
||||
await waitFor(() => expect(screen.getAllByRole("button", { name: "Copy" })).toHaveLength(2));
|
||||
});
|
||||
|
||||
it("does not navigate away when clicking the chat title", async () => {
|
||||
const client = makeClient();
|
||||
const onGoHome = vi.fn();
|
||||
|
||||
Reference in New Issue
Block a user