feat(FN-2152): render collapsed tool call previews in chat
- Emit tool_start/tool_end SSE events from dashboard chat backend and parse them in streaming client helpers - Track in-flight and completed tool calls in useChat/useQuickChat to preserve tool output summaries alongside assistant messages - Render collapsed tool call preview blocks in ChatView and QuickChatFAB with dedicated tokenized styles for compact output summaries - Expand frontend and backend test coverage for SSE tool events, hook state transitions, and collapsed preview rendering behavior - Add a changeset for @gsxdsm/fusion documenting the new tool-call display behavior
This commit is contained in:
@@ -309,6 +309,96 @@ describe("ChatManager.sendMessage", () => {
|
||||
expect(assistantCall?.[1].content).toBe("Hello world!");
|
||||
});
|
||||
|
||||
|
||||
it("broadcasts tool_start and tool_end SSE events when agent calls tools", async () => {
|
||||
const events: Array<{ type: string; data: unknown }> = [];
|
||||
const unsubscribe = chatStreamManager.subscribe("chat-001", (event) => {
|
||||
events.push(event);
|
||||
});
|
||||
|
||||
let onToolStartCb: ((name: string, args?: Record<string, unknown>) => void) | undefined;
|
||||
let onToolEndCb: ((name: string, isError: boolean, result?: unknown) => void) | undefined;
|
||||
|
||||
__setCreateFnAgent(async (options: any) => {
|
||||
onToolStartCb = options.onToolStart;
|
||||
onToolEndCb = options.onToolEnd;
|
||||
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
onToolStartCb?.("read", { path: "/foo.ts" });
|
||||
onToolEndCb?.("read", false, "file contents");
|
||||
options.onText?.("Done");
|
||||
}),
|
||||
dispose: vi.fn(),
|
||||
state: {
|
||||
messages: [{ role: "assistant", content: "Done" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const chatManager = createChatManager();
|
||||
await chatManager.sendMessage("chat-001", "Use read tool");
|
||||
unsubscribe();
|
||||
|
||||
expect(events).toContainEqual({
|
||||
type: "tool_start",
|
||||
data: { toolName: "read", args: { path: "/foo.ts" } },
|
||||
});
|
||||
expect(events).toContainEqual({
|
||||
type: "tool_end",
|
||||
data: { toolName: "read", isError: false, result: "file contents" },
|
||||
});
|
||||
});
|
||||
|
||||
it("persists tool calls in assistant message metadata", async () => {
|
||||
let onToolStartCb: ((name: string, args?: Record<string, unknown>) => void) | undefined;
|
||||
let onToolEndCb: ((name: string, isError: boolean, result?: unknown) => void) | undefined;
|
||||
|
||||
__setCreateFnAgent(async (options: any) => {
|
||||
onToolStartCb = options.onToolStart;
|
||||
onToolEndCb = options.onToolEnd;
|
||||
|
||||
return {
|
||||
session: {
|
||||
prompt: vi.fn().mockImplementation(async () => {
|
||||
onToolStartCb?.("read", { path: "foo.ts" });
|
||||
onToolEndCb?.("read", false, "contents");
|
||||
options.onText?.("Here you go");
|
||||
}),
|
||||
dispose: vi.fn(),
|
||||
state: {
|
||||
messages: [{ role: "assistant", content: "Here you go" }],
|
||||
},
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
const chatManager = createChatManager();
|
||||
await chatManager.sendMessage("chat-001", "Read foo.ts");
|
||||
|
||||
const assistantCall = mockChatStore.addMessage.mock.calls.find(
|
||||
(call) => call[1].role === "assistant",
|
||||
);
|
||||
|
||||
expect(assistantCall).toBeDefined();
|
||||
expect(assistantCall?.[1]).toEqual(
|
||||
expect.objectContaining({
|
||||
metadata: {
|
||||
toolCalls: [
|
||||
{
|
||||
toolName: "read",
|
||||
args: { path: "foo.ts" },
|
||||
isError: false,
|
||||
result: "contents",
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("creates chat agents with the full coding toolset", async () => {
|
||||
let createOptions: any;
|
||||
__setCreateFnAgent(async (options: any) => {
|
||||
|
||||
Reference in New Issue
Block a user