feat: add CLI Apps settings MVP
This commit is contained in:
@@ -2,7 +2,24 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { AgentActivityCluster } from "@/components/thread/AgentActivityCluster";
|
||||
import type { UIMessage } from "@/lib/types";
|
||||
import type { CliAppInfo, UIMessage } from "@/lib/types";
|
||||
|
||||
const BLENDER_CLI_APP: CliAppInfo = {
|
||||
name: "blender",
|
||||
display_name: "Blender",
|
||||
category: "3d",
|
||||
description: "3D creation",
|
||||
requires: "",
|
||||
source: "harness",
|
||||
entry_point: "cli-anything-blender",
|
||||
install_supported: true,
|
||||
installed: true,
|
||||
available: true,
|
||||
status: "installed",
|
||||
logo_url: "https://example.invalid/blender.svg",
|
||||
brand_color: "#E87D0D",
|
||||
skill_installed: true,
|
||||
};
|
||||
|
||||
function activityMessages(extraReasoning = "", extraTool?: UIMessage): UIMessage[] {
|
||||
const rows: UIMessage[] = [
|
||||
@@ -271,6 +288,67 @@ describe("AgentActivityCluster", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("renders CLI app runs as dedicated activity rows", () => {
|
||||
const line = 'run_cli_app({"name":"blender","args":["--background","scene.blend"],"json":true})';
|
||||
render(
|
||||
<AgentActivityCluster
|
||||
messages={[{
|
||||
id: "t-cli",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: line,
|
||||
traces: [line],
|
||||
createdAt: 1,
|
||||
}]}
|
||||
isTurnStreaming
|
||||
hasBodyBelow={false}
|
||||
cliApps={[BLENDER_CLI_APP]}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /running cli @blender/i }));
|
||||
|
||||
const cliRuns = screen.getByTestId("activity-cli-runs");
|
||||
expect(cliRuns).toHaveTextContent("Running CLI");
|
||||
expect(cliRuns).toHaveTextContent("@blender");
|
||||
expect(cliRuns).toHaveTextContent("--json --background scene.blend");
|
||||
expect(screen.getByTestId("activity-cli-logo-blender")).toBeInTheDocument();
|
||||
expect(screen.queryByText(/run_cli_app/)).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("labels rejected CLI app calls as failed instead of ran", () => {
|
||||
render(
|
||||
<AgentActivityCluster
|
||||
messages={[{
|
||||
id: "t-cli-fail",
|
||||
role: "tool",
|
||||
kind: "trace",
|
||||
content: 'run_cli_app({"name":"github","args":["repo","view"],"json":"true"})',
|
||||
traces: ['run_cli_app({"name":"github","args":["repo","view"],"json":"true"})'],
|
||||
toolEvents: [
|
||||
{
|
||||
phase: "error",
|
||||
call_id: "call-github",
|
||||
name: "run_cli_app",
|
||||
arguments: { name: "github", args: ["repo", "view"], json: "true" },
|
||||
error: "Error: CLI app 'github' not found",
|
||||
},
|
||||
],
|
||||
createdAt: 1,
|
||||
}]}
|
||||
isTurnStreaming={false}
|
||||
hasBodyBelow={false}
|
||||
/>,
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: /cli failed @github/i }));
|
||||
|
||||
expect(screen.getByTestId("activity-cli-runs")).toHaveTextContent("CLI failed");
|
||||
expect(screen.getByTestId("activity-cli-runs")).toHaveTextContent("@github");
|
||||
expect(screen.getByTestId("activity-cli-runs")).toHaveTextContent("Error: CLI app 'github' not found");
|
||||
expect(screen.queryByText("Ran CLI")).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("does not render zero diff counters for completed edits", () => {
|
||||
render(
|
||||
<AgentActivityCluster
|
||||
|
||||
@@ -2,10 +2,12 @@ import { beforeEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import {
|
||||
deleteSession,
|
||||
fetchCliApps,
|
||||
fetchSidebarState,
|
||||
fetchWebuiThread,
|
||||
listSessions,
|
||||
listSlashCommands,
|
||||
runCliAppAction,
|
||||
updateSidebarState,
|
||||
updateImageGenerationSettings,
|
||||
updateProviderSettings,
|
||||
@@ -116,6 +118,33 @@ describe("webui API helpers", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("reads CLI Apps catalog and serializes actions", async () => {
|
||||
vi.mocked(fetch).mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: async () => ({
|
||||
apps: [],
|
||||
installed_count: 0,
|
||||
catalog_updated_at: "2026-04-18",
|
||||
}),
|
||||
} as Response);
|
||||
|
||||
await expect(fetchCliApps("tok")).resolves.toMatchObject({ apps: [] });
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/settings/cli-apps",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
);
|
||||
|
||||
await runCliAppAction("tok", "install", "gimp");
|
||||
expect(fetch).toHaveBeenCalledWith(
|
||||
"/api/settings/cli-apps/install?name=gimp",
|
||||
expect.objectContaining({
|
||||
headers: { Authorization: "Bearer tok" },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("reads and writes persisted sidebar state", async () => {
|
||||
const state = {
|
||||
schema_version: 1,
|
||||
|
||||
@@ -727,6 +727,8 @@ describe("App layout", () => {
|
||||
expect(within(settingsNav).getByRole("button", { name: "Web" })).toBeInTheDocument();
|
||||
expect(within(settingsNav).getByRole("button", { name: "Advanced" })).toBeInTheDocument();
|
||||
expect(screen.getByRole("button", { name: "Sign out" })).toBeInTheDocument();
|
||||
fireEvent.click(within(settingsNav).getByRole("button", { name: "Appearance" }));
|
||||
expect(screen.getByText("Brand logos")).toBeInTheDocument();
|
||||
fireEvent.click(within(settingsNav).getByRole("button", { name: "Models" }));
|
||||
expect(screen.getByText("AI")).toBeInTheDocument();
|
||||
const modelInput = screen.getByDisplayValue("openai/gpt-4o");
|
||||
@@ -739,6 +741,8 @@ describe("App layout", () => {
|
||||
fireEvent.click(within(settingsNav).getByRole("button", { name: "Providers" }));
|
||||
expect(screen.getByText("OpenRouter")).toBeInTheDocument();
|
||||
expect(screen.getByText("Ant Ling")).toBeInTheDocument();
|
||||
expect(screen.getByTestId("provider-logo-openai")).toBeInTheDocument();
|
||||
expect(screen.getByText(/Product names, logos, and brands/)).toBeInTheDocument();
|
||||
expect(screen.getAllByText("Not configured").length).toBeGreaterThan(0);
|
||||
fireEvent.click(screen.getByText("OpenAI"));
|
||||
fireEvent.click(screen.getByRole("button", { name: "Edit" }));
|
||||
|
||||
@@ -2,7 +2,42 @@ import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { MessageBubble } from "@/components/MessageBubble";
|
||||
import type { UIMessage } from "@/lib/types";
|
||||
import type { CliAppInfo, UIMessage } from "@/lib/types";
|
||||
|
||||
const CLI_APPS: CliAppInfo[] = [
|
||||
{
|
||||
name: "zoom",
|
||||
display_name: "Zoom",
|
||||
category: "productivity",
|
||||
description: "Meetings",
|
||||
requires: "",
|
||||
source: "harness",
|
||||
entry_point: "cli-anything-zoom",
|
||||
install_supported: true,
|
||||
installed: true,
|
||||
available: true,
|
||||
status: "installed",
|
||||
logo_url: "https://example.invalid/zoom.svg",
|
||||
brand_color: "#0B5CFF",
|
||||
skill_installed: true,
|
||||
},
|
||||
{
|
||||
name: "krita",
|
||||
display_name: "Krita",
|
||||
category: "image",
|
||||
description: "Painting",
|
||||
requires: "",
|
||||
source: "harness",
|
||||
entry_point: "cli-anything-krita",
|
||||
install_supported: true,
|
||||
installed: false,
|
||||
available: false,
|
||||
status: "not_installed",
|
||||
logo_url: null,
|
||||
brand_color: "#3BABFF",
|
||||
skill_installed: false,
|
||||
},
|
||||
];
|
||||
|
||||
describe("MessageBubble", () => {
|
||||
it("renders user messages as right-aligned pills", () => {
|
||||
@@ -22,6 +57,53 @@ describe("MessageBubble", () => {
|
||||
expect(screen.queryByRole("button", { name: "Copy reply" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders installed CLI app mentions inside sent user messages", () => {
|
||||
const message: UIMessage = {
|
||||
id: "u-cli",
|
||||
role: "user",
|
||||
content: "Hi nano, please use @zoom to book a meeting, not @krita",
|
||||
createdAt: Date.now(),
|
||||
};
|
||||
|
||||
render(<MessageBubble message={message} cliApps={CLI_APPS} />);
|
||||
|
||||
const token = screen.getByTestId("message-cli-mention-zoom");
|
||||
expect(token).toHaveTextContent("@zoom");
|
||||
expect(token.className).not.toContain("rounded");
|
||||
expect(token.className).not.toContain("px-");
|
||||
expect(token.getAttribute("style")).toContain("color: #0B5CFF");
|
||||
expect(token.getAttribute("style")).toContain("text-shadow");
|
||||
expect(screen.getByTestId("message-cli-mention-logo-zoom")).toBeInTheDocument();
|
||||
expect(screen.queryByTestId("message-cli-mention-krita")).not.toBeInTheDocument();
|
||||
expect(screen.getByText(/not @krita/)).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("renders structured CLI app attachments even without the installed catalog", () => {
|
||||
const message: UIMessage = {
|
||||
id: "u-cli-attached",
|
||||
role: "user",
|
||||
content: "Please use @drawio for the diagram",
|
||||
createdAt: Date.now(),
|
||||
cliApps: [{
|
||||
name: "drawio",
|
||||
display_name: "Draw.io",
|
||||
category: "diagram",
|
||||
entry_point: "cli-anything-drawio",
|
||||
logo_url: "https://example.invalid/drawio.svg",
|
||||
brand_color: "#F08705",
|
||||
}],
|
||||
};
|
||||
|
||||
render(<MessageBubble message={message} cliApps={[]} />);
|
||||
|
||||
const token = screen.getByTestId("message-cli-mention-drawio");
|
||||
expect(token).toHaveTextContent("@drawio");
|
||||
expect(token.className).not.toContain("rounded");
|
||||
expect(token.className).not.toContain("px-");
|
||||
expect(token.getAttribute("style")).toContain("color: #F08705");
|
||||
expect(screen.getByTestId("message-cli-mention-logo-drawio")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("copies completed assistant replies from the action row", async () => {
|
||||
const writeText = vi.fn().mockResolvedValue(undefined);
|
||||
Object.defineProperty(navigator, "clipboard", {
|
||||
|
||||
@@ -332,6 +332,49 @@ describe("NanobotClient", () => {
|
||||
);
|
||||
});
|
||||
|
||||
it("includes CLI app attachments in outbound messages", () => {
|
||||
const client = new NanobotClient({
|
||||
url: "ws://test",
|
||||
reconnect: false,
|
||||
socketFactory: (url) => new FakeSocket(url) as unknown as WebSocket,
|
||||
});
|
||||
client.connect();
|
||||
lastSocket().fakeOpen();
|
||||
|
||||
client.sendMessage(
|
||||
"chat-cli",
|
||||
"@drawio please make this diagram",
|
||||
undefined,
|
||||
{
|
||||
cliApps: [{
|
||||
name: "drawio",
|
||||
display_name: "Draw.io",
|
||||
category: "diagrams",
|
||||
entry_point: "cli-anything-drawio",
|
||||
logo_url: null,
|
||||
brand_color: "#F08705",
|
||||
}],
|
||||
},
|
||||
);
|
||||
|
||||
expect(lastSocket().sent).toContain(
|
||||
JSON.stringify({
|
||||
type: "message",
|
||||
chat_id: "chat-cli",
|
||||
content: "@drawio please make this diagram",
|
||||
cli_apps: [{
|
||||
name: "drawio",
|
||||
display_name: "Draw.io",
|
||||
category: "diagrams",
|
||||
entry_point: "cli-anything-drawio",
|
||||
logo_url: null,
|
||||
brand_color: "#F08705",
|
||||
}],
|
||||
webui: true,
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("re-attaches known chats after a reconnect", async () => {
|
||||
const client = new NanobotClient({
|
||||
url: "ws://test",
|
||||
|
||||
@@ -2,7 +2,7 @@ import { fireEvent, render, screen, waitFor } from "@testing-library/react";
|
||||
import { afterEach, describe, expect, it, vi } from "vitest";
|
||||
|
||||
import { ThreadComposer } from "@/components/thread/ThreadComposer";
|
||||
import type { SlashCommand } from "@/lib/types";
|
||||
import type { CliAppInfo, SlashCommand } from "@/lib/types";
|
||||
|
||||
const COMMANDS: SlashCommand[] = [
|
||||
{
|
||||
@@ -19,6 +19,57 @@ const COMMANDS: SlashCommand[] = [
|
||||
argHint: "[n]",
|
||||
},
|
||||
];
|
||||
|
||||
const CLI_APPS: CliAppInfo[] = [
|
||||
{
|
||||
name: "gimp",
|
||||
display_name: "GIMP",
|
||||
category: "image",
|
||||
description: "Image editing",
|
||||
requires: "",
|
||||
source: "harness",
|
||||
entry_point: "cli-anything-gimp",
|
||||
install_supported: true,
|
||||
installed: true,
|
||||
available: true,
|
||||
status: "installed",
|
||||
logo_url: "https://example.invalid/gimp.svg",
|
||||
brand_color: "#5C5543",
|
||||
skill_installed: true,
|
||||
},
|
||||
{
|
||||
name: "blender",
|
||||
display_name: "Blender",
|
||||
category: "3d",
|
||||
description: "3D creation",
|
||||
requires: "",
|
||||
source: "harness",
|
||||
entry_point: "cli-anything-blender",
|
||||
install_supported: true,
|
||||
installed: true,
|
||||
available: true,
|
||||
status: "installed",
|
||||
logo_url: null,
|
||||
brand_color: "#E87D0D",
|
||||
skill_installed: true,
|
||||
},
|
||||
{
|
||||
name: "krita",
|
||||
display_name: "Krita",
|
||||
category: "image",
|
||||
description: "Painting",
|
||||
requires: "",
|
||||
source: "harness",
|
||||
entry_point: "cli-anything-krita",
|
||||
install_supported: true,
|
||||
installed: false,
|
||||
available: false,
|
||||
status: "not_installed",
|
||||
logo_url: null,
|
||||
brand_color: "#3BABFF",
|
||||
skill_installed: false,
|
||||
},
|
||||
];
|
||||
const ORIGINAL_INNER_HEIGHT = window.innerHeight;
|
||||
|
||||
afterEach(() => {
|
||||
@@ -66,7 +117,7 @@ describe("ThreadComposer", () => {
|
||||
const input = screen.getByPlaceholderText("Ask anything...");
|
||||
expect(input).toBeInTheDocument();
|
||||
expect(input.className).toContain("min-h-[78px]");
|
||||
expect(input.parentElement?.className).toContain("max-w-[58rem]");
|
||||
expect(input.parentElement?.parentElement?.className).toContain("max-w-[58rem]");
|
||||
});
|
||||
|
||||
it("keeps the thread composer compact while matching the hero style", () => {
|
||||
@@ -81,9 +132,9 @@ describe("ThreadComposer", () => {
|
||||
expect(screen.getByText("gpt-4o")).toBeInTheDocument();
|
||||
const input = screen.getByPlaceholderText("Type your message...");
|
||||
expect(input.className).toContain("min-h-[50px]");
|
||||
expect(input.parentElement?.className).toContain("max-w-[49.5rem]");
|
||||
expect(input.parentElement?.className).toContain("rounded-[22px]");
|
||||
expect(input.parentElement?.className).toContain("shadow-[0_12px_30px_rgba(15,23,42,0.07)]");
|
||||
expect(input.parentElement?.parentElement?.className).toContain("max-w-[49.5rem]");
|
||||
expect(input.parentElement?.parentElement?.className).toContain("rounded-[22px]");
|
||||
expect(input.parentElement?.parentElement?.className).toContain("shadow-[0_12px_30px_rgba(15,23,42,0.07)]");
|
||||
expect(screen.getByRole("button", { name: "Attach image" }).className).toContain("bg-card");
|
||||
expect(screen.getByRole("button", { name: "Send message" }).className).toContain("bg-foreground");
|
||||
});
|
||||
@@ -163,6 +214,123 @@ describe("ThreadComposer", () => {
|
||||
expect(screen.queryByRole("listbox", { name: "Slash commands" })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("opens the CLI app mention palette and inserts the selected app", () => {
|
||||
const onSend = vi.fn();
|
||||
render(
|
||||
<ThreadComposer
|
||||
onSend={onSend}
|
||||
placeholder="Type your message..."
|
||||
cliApps={CLI_APPS}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("Message input");
|
||||
fireEvent.change(input, { target: { value: "@", selectionStart: 1 } });
|
||||
|
||||
const palette = screen.getByRole("listbox", { name: "CLI Apps" });
|
||||
expect(palette).toBeInTheDocument();
|
||||
expect(screen.getByRole("option", { name: /@gimp/i })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
expect(screen.queryByRole("option", { name: /@krita/i })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.keyDown(input, { key: "ArrowDown" });
|
||||
expect(screen.getByRole("option", { name: /@blender/i })).toHaveAttribute(
|
||||
"aria-selected",
|
||||
"true",
|
||||
);
|
||||
fireEvent.keyDown(input, { key: "Enter" });
|
||||
|
||||
expect(input).toHaveValue("@blender ");
|
||||
expect(screen.getByTestId("composer-cli-mention-blender")).toHaveTextContent("@blender");
|
||||
expect(screen.queryByTestId("composer-cli-app-tray")).not.toBeInTheDocument();
|
||||
expect(onSend).not.toHaveBeenCalled();
|
||||
expect(screen.queryByRole("listbox", { name: "CLI Apps" })).not.toBeInTheDocument();
|
||||
|
||||
fireEvent.click(screen.getByRole("button", { name: "Send message" }));
|
||||
|
||||
expect(onSend).toHaveBeenCalledWith("@blender", undefined, {
|
||||
cliApps: [{
|
||||
name: "blender",
|
||||
display_name: "Blender",
|
||||
category: "3d",
|
||||
entry_point: "cli-anything-blender",
|
||||
logo_url: null,
|
||||
brand_color: "#E87D0D",
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
it("completes a CLI app mention with Tab and adds exactly one trailing space", () => {
|
||||
render(
|
||||
<ThreadComposer
|
||||
onSend={vi.fn()}
|
||||
placeholder="Type your message..."
|
||||
cliApps={CLI_APPS}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("Message input");
|
||||
fireEvent.change(input, {
|
||||
target: { value: "use @ble", selectionStart: 8 },
|
||||
});
|
||||
|
||||
fireEvent.keyDown(input, { key: "Tab" });
|
||||
|
||||
expect(input).toHaveValue("use @blender ");
|
||||
expect(screen.getByTestId("composer-cli-mention-blender")).toHaveTextContent("@blender");
|
||||
});
|
||||
|
||||
it("does not duplicate the next word separator when completing a CLI app mention", () => {
|
||||
render(
|
||||
<ThreadComposer
|
||||
onSend={vi.fn()}
|
||||
placeholder="Type your message..."
|
||||
cliApps={CLI_APPS}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("Message input");
|
||||
fireEvent.change(input, {
|
||||
target: { value: "use @ble tonight", selectionStart: 8 },
|
||||
});
|
||||
|
||||
fireEvent.keyDown(input, { key: "Tab" });
|
||||
|
||||
expect(input).toHaveValue("use @blender tonight");
|
||||
});
|
||||
|
||||
it("renders a CLI app mention logo inline without moving the text cursor slot", () => {
|
||||
render(
|
||||
<ThreadComposer
|
||||
onSend={vi.fn()}
|
||||
placeholder="Type your message..."
|
||||
cliApps={CLI_APPS}
|
||||
/>,
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText("Message input");
|
||||
fireEvent.change(input, {
|
||||
target: { value: "meeting in @gimp", selectionStart: 16 },
|
||||
});
|
||||
|
||||
expect(input).toHaveValue("meeting in @gimp");
|
||||
const token = screen.getByTestId("composer-cli-mention-gimp");
|
||||
expect(token).toHaveTextContent("@gimp");
|
||||
expect(token.className).not.toContain("font-semibold");
|
||||
expect(token.className).not.toContain("zoom-in");
|
||||
expect(token.className).not.toContain("px-");
|
||||
expect(token.className).not.toContain("mx-");
|
||||
expect(token.getAttribute("style")).toContain("color: #5C5543");
|
||||
expect(token.getAttribute("style")).toContain("text-shadow");
|
||||
expect(screen.queryByTestId("composer-cli-app-tray")).not.toBeInTheDocument();
|
||||
const logo = screen.getByTestId("composer-cli-mention-logo-gimp");
|
||||
expect(logo.className).toContain("top-1/2");
|
||||
expect(logo.className).toContain("left-1/2");
|
||||
expect(logo.className).not.toContain("-top-");
|
||||
});
|
||||
|
||||
it("opens the slash command palette downward when there is more room below", async () => {
|
||||
vi.spyOn(HTMLFormElement.prototype, "getBoundingClientRect").mockReturnValue(
|
||||
rect({ top: 40, bottom: 160, width: 800, height: 120 }),
|
||||
|
||||
@@ -356,6 +356,58 @@ describe("useNanobotStream", () => {
|
||||
'exec({"cmd":"ls"})',
|
||||
'read_file({"path":"notes.md"})',
|
||||
]);
|
||||
expect(result.current.messages[0].toolEvents).toMatchObject([
|
||||
{ phase: "end", call_id: "call-exec", name: "exec" },
|
||||
{ phase: "error", call_id: "call-read", name: "read_file", error: "missing" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("keeps phase updates when a tool event trace line is deduped", () => {
|
||||
const fake = fakeClient();
|
||||
const { result } = renderHook(() => useNanobotStream("chat-tool-phase", EMPTY_MESSAGES), {
|
||||
wrapper: wrap(fake.client),
|
||||
});
|
||||
|
||||
const args = { name: "github", args: ["repo", "view"], json: "true" };
|
||||
act(() => {
|
||||
fake.emit("chat-tool-phase", {
|
||||
event: "message",
|
||||
chat_id: "chat-tool-phase",
|
||||
text: "",
|
||||
kind: "tool_hint",
|
||||
tool_events: [{
|
||||
phase: "start",
|
||||
call_id: "call-cli",
|
||||
name: "run_cli_app",
|
||||
arguments: args,
|
||||
}],
|
||||
});
|
||||
fake.emit("chat-tool-phase", {
|
||||
event: "message",
|
||||
chat_id: "chat-tool-phase",
|
||||
text: "",
|
||||
kind: "progress",
|
||||
tool_events: [{
|
||||
phase: "error",
|
||||
call_id: "call-cli",
|
||||
name: "run_cli_app",
|
||||
arguments: args,
|
||||
error: "Error: CLI app 'github' not found",
|
||||
}],
|
||||
});
|
||||
});
|
||||
|
||||
expect(result.current.messages[0].traces).toEqual([
|
||||
'run_cli_app({"name":"github","args":["repo","view"],"json":"true"})',
|
||||
]);
|
||||
expect(result.current.messages[0].toolEvents).toMatchObject([
|
||||
{
|
||||
phase: "error",
|
||||
call_id: "call-cli",
|
||||
name: "run_cli_app",
|
||||
error: "Error: CLI app 'github' not found",
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it("renders live file_edit events as their own activity trace", () => {
|
||||
|
||||
Reference in New Issue
Block a user