diff --git a/ui/src/ui/chat/build-chat-items.ts b/ui/src/ui/chat/build-chat-items.ts index 1ceb80230dd..4e01f6a0faa 100644 --- a/ui/src/ui/chat/build-chat-items.ts +++ b/ui/src/ui/chat/build-chat-items.ts @@ -1,6 +1,7 @@ import type { ChatItem, MessageGroup, ToolCard } from "../types/chat-types.ts"; import { extractTextCached } from "./message-extract.ts"; -import { normalizeMessage, normalizeRoleForGrouping } from "./message-normalizer.ts"; +import { normalizeMessage } from "./message-normalizer.ts"; +import { normalizeRoleForGrouping } from "./role-normalizer.ts"; import { messageMatchesSearchQuery } from "./search-match.ts"; import { extractToolCards, extractToolPreview } from "./tool-cards.ts"; diff --git a/ui/src/ui/chat/chat-avatar.ts b/ui/src/ui/chat/chat-avatar.ts index 756a04eae5a..1992ac2e23d 100644 --- a/ui/src/ui/chat/chat-avatar.ts +++ b/ui/src/ui/chat/chat-avatar.ts @@ -10,7 +10,7 @@ import { isRenderableControlUiAvatarUrl, resolveAssistantTextAvatar, } from "../views/agents-utils.ts"; -import { normalizeRoleForGrouping } from "./message-normalizer.ts"; +import { normalizeRoleForGrouping } from "./role-normalizer.ts"; export function renderChatAvatar( role: string, diff --git a/ui/src/ui/chat/grouped-render.ts b/ui/src/ui/chat/grouped-render.ts index f298da6b3c2..4e9e59309eb 100644 --- a/ui/src/ui/chat/grouped-render.ts +++ b/ui/src/ui/chat/grouped-render.ts @@ -24,11 +24,8 @@ import { extractThinkingCached, formatReasoningMarkdown, } from "./message-extract.ts"; -import { - isToolResultMessage, - normalizeMessage, - normalizeRoleForGrouping, -} from "./message-normalizer.ts"; +import { isToolResultMessage, normalizeMessage } from "./message-normalizer.ts"; +import { normalizeRoleForGrouping } from "./role-normalizer.ts"; import { isTtsSupported, speakText, stopTts, isTtsSpeaking } from "./speech.ts"; import { extractToolCards, diff --git a/ui/src/ui/chat/message-normalizer.test.ts b/ui/src/ui/chat/message-normalizer.test.ts index 1b37981d1d4..e8a56c31021 100644 --- a/ui/src/ui/chat/message-normalizer.test.ts +++ b/ui/src/ui/chat/message-normalizer.test.ts @@ -1,9 +1,5 @@ import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"; -import { - normalizeMessage, - normalizeRoleForGrouping, - isToolResultMessage, -} from "./message-normalizer.ts"; +import { normalizeMessage } from "./message-normalizer.ts"; describe("message-normalizer", () => { describe("normalizeMessage", () => { @@ -390,69 +386,4 @@ describe("message-normalizer", () => { expect(result.senderLabel).toBe("Iris"); }); }); - - describe("normalizeRoleForGrouping", () => { - it("returns tool for toolresult", () => { - expect(normalizeRoleForGrouping("toolresult")).toBe("tool"); - expect(normalizeRoleForGrouping("toolResult")).toBe("tool"); - expect(normalizeRoleForGrouping("TOOLRESULT")).toBe("tool"); - }); - - it("returns tool for tool_result", () => { - expect(normalizeRoleForGrouping("tool_result")).toBe("tool"); - expect(normalizeRoleForGrouping("TOOL_RESULT")).toBe("tool"); - }); - - it("returns tool for tool", () => { - expect(normalizeRoleForGrouping("tool")).toBe("tool"); - expect(normalizeRoleForGrouping("Tool")).toBe("tool"); - }); - - it("returns tool for function", () => { - expect(normalizeRoleForGrouping("function")).toBe("tool"); - expect(normalizeRoleForGrouping("Function")).toBe("tool"); - }); - - it("preserves user role", () => { - expect(normalizeRoleForGrouping("user")).toBe("user"); - expect(normalizeRoleForGrouping("User")).toBe("User"); - }); - - it("preserves assistant role", () => { - expect(normalizeRoleForGrouping("assistant")).toBe("assistant"); - }); - - it("preserves system role", () => { - expect(normalizeRoleForGrouping("system")).toBe("system"); - }); - }); - - describe("isToolResultMessage", () => { - it("returns true for toolresult role", () => { - expect(isToolResultMessage({ role: "toolresult" })).toBe(true); - expect(isToolResultMessage({ role: "toolResult" })).toBe(true); - expect(isToolResultMessage({ role: "TOOLRESULT" })).toBe(true); - }); - - it("returns true for tool_result role", () => { - expect(isToolResultMessage({ role: "tool_result" })).toBe(true); - expect(isToolResultMessage({ role: "TOOL_RESULT" })).toBe(true); - }); - - it("returns false for other roles", () => { - expect(isToolResultMessage({ role: "user" })).toBe(false); - expect(isToolResultMessage({ role: "assistant" })).toBe(false); - expect(isToolResultMessage({ role: "tool" })).toBe(false); - }); - - it("returns false for missing role", () => { - expect(isToolResultMessage({})).toBe(false); - expect(isToolResultMessage({ content: "test" })).toBe(false); - }); - - it("returns false for non-string role", () => { - expect(isToolResultMessage({ role: 123 })).toBe(false); - expect(isToolResultMessage({ role: null })).toBe(false); - }); - }); }); diff --git a/ui/src/ui/chat/message-normalizer.ts b/ui/src/ui/chat/message-normalizer.ts index 697cd39377a..dc090e97daa 100644 --- a/ui/src/ui/chat/message-normalizer.ts +++ b/ui/src/ui/chat/message-normalizer.ts @@ -13,6 +13,7 @@ import { mediaKindFromMime } from "../../../../src/media/constants.js"; import { splitMediaFromOutput } from "../../../../src/media/parse.js"; import { parseInlineDirectives } from "../../../../src/utils/directive-tags.js"; import type { NormalizedMessage, MessageContentItem } from "../types/chat-types.ts"; +export { isToolResultMessage, normalizeRoleForGrouping } from "./role-normalizer.ts"; function coerceCanvasPreview( value: unknown, @@ -389,39 +390,3 @@ export function normalizeMessage(message: unknown): NormalizedMessage { ...(replyTarget ? { replyTarget } : {}), }; } - -/** - * Normalize role for grouping purposes. - */ -export function normalizeRoleForGrouping(role: string): string { - const lower = role.toLowerCase(); - // Preserve original casing when it's already a core role. - if (role === "user" || role === "User") { - return role; - } - if (role === "assistant") { - return "assistant"; - } - if (role === "system") { - return "system"; - } - // Keep tool-related roles distinct so the UI can style/toggle them. - if ( - lower === "toolresult" || - lower === "tool_result" || - lower === "tool" || - lower === "function" - ) { - return "tool"; - } - return role; -} - -/** - * Check if a message is a tool result message based on its role. - */ -export function isToolResultMessage(message: unknown): boolean { - const m = message as Record; - const role = typeof m.role === "string" ? m.role.toLowerCase() : ""; - return role === "toolresult" || role === "tool_result"; -} diff --git a/ui/src/ui/chat/role-normalizer.test.ts b/ui/src/ui/chat/role-normalizer.test.ts new file mode 100644 index 00000000000..014de0e5fc8 --- /dev/null +++ b/ui/src/ui/chat/role-normalizer.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { isToolResultMessage, normalizeRoleForGrouping } from "./role-normalizer.ts"; + +describe("normalizeRoleForGrouping", () => { + it("returns tool for tool result role variants", () => { + expect(normalizeRoleForGrouping("toolresult")).toBe("tool"); + expect(normalizeRoleForGrouping("toolResult")).toBe("tool"); + expect(normalizeRoleForGrouping("TOOLRESULT")).toBe("tool"); + expect(normalizeRoleForGrouping("tool_result")).toBe("tool"); + expect(normalizeRoleForGrouping("TOOL_RESULT")).toBe("tool"); + }); + + it("returns tool for tool and function roles", () => { + expect(normalizeRoleForGrouping("tool")).toBe("tool"); + expect(normalizeRoleForGrouping("Tool")).toBe("tool"); + expect(normalizeRoleForGrouping("function")).toBe("tool"); + expect(normalizeRoleForGrouping("Function")).toBe("tool"); + }); + + it("preserves core roles", () => { + expect(normalizeRoleForGrouping("user")).toBe("user"); + expect(normalizeRoleForGrouping("User")).toBe("User"); + expect(normalizeRoleForGrouping("assistant")).toBe("assistant"); + expect(normalizeRoleForGrouping("system")).toBe("system"); + }); + + it("detects only tool result role variants", () => { + expect(isToolResultMessage({ role: "toolresult" })).toBe(true); + expect(isToolResultMessage({ role: "toolResult" })).toBe(true); + expect(isToolResultMessage({ role: "TOOLRESULT" })).toBe(true); + expect(isToolResultMessage({ role: "tool_result" })).toBe(true); + expect(isToolResultMessage({ role: "TOOL_RESULT" })).toBe(true); + expect(isToolResultMessage({ role: "user" })).toBe(false); + expect(isToolResultMessage({ role: "assistant" })).toBe(false); + expect(isToolResultMessage({ role: "tool" })).toBe(false); + expect(isToolResultMessage({})).toBe(false); + expect(isToolResultMessage({ content: "test" })).toBe(false); + expect(isToolResultMessage({ role: 123 })).toBe(false); + expect(isToolResultMessage({ role: null })).toBe(false); + }); +}); diff --git a/ui/src/ui/chat/role-normalizer.ts b/ui/src/ui/chat/role-normalizer.ts new file mode 100644 index 00000000000..952e6a736fc --- /dev/null +++ b/ui/src/ui/chat/role-normalizer.ts @@ -0,0 +1,35 @@ +/** + * Normalize role for grouping purposes. + */ +export function normalizeRoleForGrouping(role: string): string { + const lower = role.toLowerCase(); + // Preserve original casing when it's already a core role. + if (role === "user" || role === "User") { + return role; + } + if (role === "assistant") { + return "assistant"; + } + if (role === "system") { + return "system"; + } + // Keep tool-related roles distinct so the UI can style/toggle them. + if ( + lower === "toolresult" || + lower === "tool_result" || + lower === "tool" || + lower === "function" + ) { + return "tool"; + } + return role; +} + +/** + * Check if a message is a tool result message based on its role. + */ +export function isToolResultMessage(message: unknown): boolean { + const m = message as Record; + const role = typeof m.role === "string" ? m.role.toLowerCase() : ""; + return role === "toolresult" || role === "tool_result"; +} diff --git a/ui/src/ui/chat/tool-cards.ts b/ui/src/ui/chat/tool-cards.ts index f1844a454dd..2ef78533242 100644 --- a/ui/src/ui/chat/tool-cards.ts +++ b/ui/src/ui/chat/tool-cards.ts @@ -7,7 +7,7 @@ import type { SidebarContent } from "../sidebar-content.ts"; import { formatToolDetail, resolveToolDisplay } from "../tool-display.ts"; import type { ToolCard } from "../types/chat-types.ts"; import { extractTextCached } from "./message-extract.ts"; -import { isToolResultMessage } from "./message-normalizer.ts"; +import { isToolResultMessage } from "./role-normalizer.ts"; import { formatToolOutputForSidebar, getTruncatedPreview } from "./tool-helpers.ts"; export type ToolPreview = NonNullable; diff --git a/ui/src/ui/chat/tool-expansion-state.ts b/ui/src/ui/chat/tool-expansion-state.ts index 7f0a6c8da81..214caa980a4 100644 --- a/ui/src/ui/chat/tool-expansion-state.ts +++ b/ui/src/ui/chat/tool-expansion-state.ts @@ -1,5 +1,5 @@ import type { ChatItem, MessageGroup } from "../types/chat-types.ts"; -import { isToolResultMessage, normalizeRoleForGrouping } from "./message-normalizer.ts"; +import { isToolResultMessage, normalizeRoleForGrouping } from "./role-normalizer.ts"; import { getOrCreateSessionCacheValue } from "./session-cache.ts"; import { extractToolCards } from "./tool-cards.ts";