feat(FN-1856): rename chat agent to Fusion and add model indicator tag
- Rename chat agent from generic label to 'Fusion' in ChatView header - Add model indicator tag displaying the current AI model name - Fix GPT model name formatting to use proper display name - Add CSS styling for the model tag pill component - Add changeset for minor version bump of @gsxdsm/fusion - Update and add ChatView tests for Fusion name and model tag
This commit is contained in:
@@ -35,6 +35,72 @@ function formatRelativeTime(dateStr: string): string {
|
||||
return date.toLocaleDateString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a model provider and ID into a human-readable tag.
|
||||
* Returns null if provider or modelId is missing/empty.
|
||||
*/
|
||||
function formatModelTag(provider?: string | null, modelId?: string | null): string | null {
|
||||
if (!provider || !modelId) return null;
|
||||
|
||||
// Handle known provider/model patterns
|
||||
const normalizedModel = modelId.toLowerCase();
|
||||
|
||||
// Claude models: "claude-sonnet-4-5" -> "Claude Sonnet 4.5"
|
||||
if (normalizedModel.includes("claude")) {
|
||||
let formatted = modelId
|
||||
.replace(/^claude[- ]/i, "Claude ")
|
||||
.replace(/sonnet[- ](\d+)[- ](\d+)/i, "Sonnet $1.$2")
|
||||
.replace(/sonnet[- ](\d+)/i, "Sonnet $1")
|
||||
.replace(/haiku[- ](\d+)/i, "Haiku $1")
|
||||
.replace(/opus[- ](\d+)/i, "Opus $1")
|
||||
.replace(/sonnet/i, "Sonnet")
|
||||
.replace(/haiku/i, "Haiku")
|
||||
.replace(/opus/i, "Opus")
|
||||
.replace(/-/g, " ")
|
||||
.trim();
|
||||
// Fix double spaces
|
||||
formatted = formatted.replace(/\s+/g, " ");
|
||||
return formatted.length > 30 ? formatted.slice(0, 30) + "…" : formatted;
|
||||
}
|
||||
|
||||
// OpenAI models: "gpt-4o" -> "GPT-4o", "gpt-4-turbo" -> "GPT-4 Turbo"
|
||||
if (normalizedModel.includes("gpt") || normalizedModel.includes("openai")) {
|
||||
// Format GPT model names: handle special cases first, then capitalize
|
||||
// Note: We don't replace hyphens globally because special cases preserve them
|
||||
const formatted = modelId
|
||||
.replace(/^gpt-4-turbo$/i, "GPT-4 Turbo")
|
||||
.replace(/^gpt-4o-mini$/i, "GPT-4o Mini")
|
||||
.replace(/^gpt-4o$/i, "GPT-4o")
|
||||
.replace(/^gpt-4$/i, "GPT-4")
|
||||
.replace(/^gpt-o1-preview$/i, "GPT-o1 Preview")
|
||||
.replace(/^gpt-o1-mini$/i, "GPT-o1 Mini")
|
||||
.replace(/^gpt-o1$/i, "GPT-o1")
|
||||
.replace(/^gpt/i, "GPT") // Capitalize remaining GPT prefix
|
||||
.trim();
|
||||
return formatted.length > 30 ? formatted.slice(0, 30) + "…" : formatted;
|
||||
}
|
||||
|
||||
// Gemini models: "gemini-2.5-pro" -> "Gemini 2.5 Pro"
|
||||
if (normalizedModel.includes("gemini")) {
|
||||
let formatted = modelId
|
||||
.replace(/^gemini[- ]/i, "Gemini ")
|
||||
.replace(/pro[- ](\d+)[- ](\d+)/i, "Pro $1.$2")
|
||||
.replace(/pro[- ](\d+)/i, "Pro $1")
|
||||
.replace(/-/g, " ")
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return formatted.length > 30 ? formatted.slice(0, 30) + "…" : formatted;
|
||||
}
|
||||
|
||||
// Generic fallback: capitalize first letter, replace hyphens with spaces
|
||||
let formatted = modelId
|
||||
.replace(/-/g, " ")
|
||||
.replace(/^\w/, (c) => c.toUpperCase())
|
||||
.replace(/\s+/g, " ")
|
||||
.trim();
|
||||
return formatted.length > 30 ? formatted.slice(0, 30) + "…" : formatted;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constant agent ID for the built-in kb agent.
|
||||
* The chat system always uses createKbAgent with CHAT_SYSTEM_PROMPT regardless
|
||||
@@ -350,7 +416,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
||||
{session.lastMessagePreview || "No messages"}
|
||||
</div>
|
||||
<div className="chat-session-meta">
|
||||
<span>{agentsMap.get(session.agentId)?.name || (session.agentId === KB_AGENT_ID ? "AI Assistant" : session.agentId.slice(0, 30))}</span>
|
||||
<span>{agentsMap.get(session.agentId)?.name || (session.agentId === KB_AGENT_ID ? "Fusion" : session.agentId.slice(0, 30))}</span>
|
||||
<span>{session.updatedAt ? formatRelativeTime(session.updatedAt) : ""}</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -420,8 +486,14 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
||||
)}
|
||||
<Bot size={16} />
|
||||
<span className="chat-thread-header-title">
|
||||
{activeSession?.title || agentsMap.get(activeSession?.agentId ?? "")?.name || activeSession?.agentId || "Chat"}
|
||||
{activeSession?.agentId === KB_AGENT_ID
|
||||
? "Fusion"
|
||||
: activeSession?.title || agentsMap.get(activeSession?.agentId ?? "")?.name || activeSession?.agentId || "Chat"}
|
||||
</span>
|
||||
{activeSession && (() => {
|
||||
const modelTag = formatModelTag(activeSession.modelProvider, activeSession.modelId);
|
||||
return modelTag ? <span className="chat-model-tag">{modelTag}</span> : null;
|
||||
})()}
|
||||
</div>
|
||||
|
||||
{/* Messages */}
|
||||
@@ -445,7 +517,11 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
||||
{message.role === "assistant" && (
|
||||
<div className="chat-message-avatar">
|
||||
<Bot size={14} />
|
||||
<span>Assistant</span>
|
||||
<span>Fusion</span>
|
||||
{activeSession && (() => {
|
||||
const modelTag = formatModelTag(activeSession.modelProvider, activeSession.modelId);
|
||||
return modelTag ? <span className="chat-model-tag">{modelTag}</span> : null;
|
||||
})()}
|
||||
</div>
|
||||
)}
|
||||
<div className="chat-message-content">{message.content}</div>
|
||||
@@ -462,7 +538,11 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
|
||||
<div className="chat-message chat-message--assistant chat-message--streaming">
|
||||
<div className="chat-message-avatar">
|
||||
<Bot size={14} />
|
||||
<span>Assistant</span>
|
||||
<span>Fusion</span>
|
||||
{activeSession && (() => {
|
||||
const modelTag = formatModelTag(activeSession.modelProvider, activeSession.modelId);
|
||||
return modelTag ? <span className="chat-model-tag">{modelTag}</span> : null;
|
||||
})()}
|
||||
</div>
|
||||
<div className="chat-message-content">{streamingText}</div>
|
||||
{streamingThinking && (
|
||||
|
||||
@@ -426,7 +426,7 @@ describe("ChatView", () => {
|
||||
expect(within(dialog!).getByText("Delete Conversation?")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows AI Assistant label for kb agent sessions in sidebar", () => {
|
||||
it("shows Fusion label for kb agent sessions in sidebar", () => {
|
||||
setupMockChat({
|
||||
sessions: [{ id: "session-001", agentId: "__kb_agent__", status: "active", title: "My Chat", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||
filteredSessions: [{ id: "session-001", agentId: "__kb_agent__", status: "active", title: "My Chat", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
||||
@@ -435,8 +435,8 @@ describe("ChatView", () => {
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const sessionItem = screen.getByTestId("chat-session-session-001");
|
||||
// Should show "AI Assistant" instead of "__kb_agent__"
|
||||
expect(within(sessionItem).getByText("AI Assistant")).toBeInTheDocument();
|
||||
// Should show "Fusion" instead of "__kb_agent__"
|
||||
expect(within(sessionItem).getByText("Fusion")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows agent ID for non-kb agent sessions in sidebar", () => {
|
||||
@@ -451,6 +451,195 @@ describe("ChatView", () => {
|
||||
// Should show the agent ID (truncated to 30 chars)
|
||||
expect(within(sessionItem).getByText("my-custom-agent")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows model tag in thread header when session has model", () => {
|
||||
setupMockChat({
|
||||
activeSession: {
|
||||
id: "session-001",
|
||||
agentId: "__kb_agent__",
|
||||
status: "active",
|
||||
title: "Test Chat",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
},
|
||||
messages: [
|
||||
{ id: "msg-001", sessionId: "session-001", role: "user", content: "Hello", createdAt: "2026-04-08T00:00:00.000Z" },
|
||||
{ id: "msg-002", sessionId: "session-001", role: "assistant", content: "Hi!", createdAt: "2026-04-08T00:01:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const modelTag = document.querySelector(".chat-model-tag");
|
||||
expect(modelTag).toBeInTheDocument();
|
||||
expect(modelTag?.textContent).toContain("Claude");
|
||||
});
|
||||
|
||||
it("does not show model tag when session has no model", () => {
|
||||
setupMockChat({
|
||||
activeSession: {
|
||||
id: "session-001",
|
||||
agentId: "__kb_agent__",
|
||||
status: "active",
|
||||
title: "Test Chat",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
},
|
||||
messages: [
|
||||
{ id: "msg-001", sessionId: "session-001", role: "user", content: "Hello", createdAt: "2026-04-08T00:00:00.000Z" },
|
||||
{ id: "msg-002", sessionId: "session-001", role: "assistant", content: "Hi!", createdAt: "2026-04-08T00:01:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const modelTag = document.querySelector(".chat-model-tag");
|
||||
expect(modelTag).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("shows model tag in message avatar when session has model", () => {
|
||||
setupMockChat({
|
||||
activeSession: {
|
||||
id: "session-001",
|
||||
agentId: "__kb_agent__",
|
||||
status: "active",
|
||||
title: "Test Chat",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
},
|
||||
messages: [
|
||||
{ id: "msg-001", sessionId: "session-001", role: "user", content: "Hello", createdAt: "2026-04-08T00:00:00.000Z" },
|
||||
{ id: "msg-002", sessionId: "session-001", role: "assistant", content: "Hi!", createdAt: "2026-04-08T00:01:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
// Find the avatar with "Fusion" text
|
||||
const avatars = document.querySelectorAll(".chat-message-avatar");
|
||||
expect(avatars.length).toBeGreaterThan(0);
|
||||
|
||||
// Check that one avatar has the model tag
|
||||
const avatarWithModelTag = Array.from(avatars).find((avatar) =>
|
||||
avatar.querySelector(".chat-model-tag"),
|
||||
);
|
||||
expect(avatarWithModelTag).toBeTruthy();
|
||||
expect(avatarWithModelTag?.querySelector(".chat-model-tag")?.textContent).toContain("GPT");
|
||||
});
|
||||
});
|
||||
|
||||
describe("formatModelTag helper function", () => {
|
||||
// Import the function for testing - we'll test it via the UI behavior instead
|
||||
// The function is not exported, so we test it indirectly through the component
|
||||
|
||||
it("formats claude-sonnet-4-5 model ID correctly", () => {
|
||||
setupMockChat({
|
||||
activeSession: {
|
||||
id: "session-001",
|
||||
agentId: "__kb_agent__",
|
||||
status: "active",
|
||||
title: "Test",
|
||||
modelProvider: "anthropic",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
},
|
||||
messages: [
|
||||
{ id: "msg-001", sessionId: "session-001", role: "assistant", content: "Hi!", createdAt: "2026-04-08T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const modelTag = document.querySelector(".chat-model-tag");
|
||||
expect(modelTag?.textContent).toContain("Claude Sonnet");
|
||||
});
|
||||
|
||||
it("formats gpt-4o model ID correctly", () => {
|
||||
setupMockChat({
|
||||
activeSession: {
|
||||
id: "session-001",
|
||||
agentId: "__kb_agent__",
|
||||
status: "active",
|
||||
title: "Test",
|
||||
modelProvider: "openai",
|
||||
modelId: "gpt-4o",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
},
|
||||
messages: [
|
||||
{ id: "msg-001", sessionId: "session-001", role: "assistant", content: "Hi!", createdAt: "2026-04-08T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const modelTag = document.querySelector(".chat-model-tag");
|
||||
expect(modelTag?.textContent).toContain("GPT-4o");
|
||||
});
|
||||
|
||||
it("formats gemini-2.5-pro model ID correctly", () => {
|
||||
setupMockChat({
|
||||
activeSession: {
|
||||
id: "session-001",
|
||||
agentId: "__kb_agent__",
|
||||
status: "active",
|
||||
title: "Test",
|
||||
modelProvider: "google",
|
||||
modelId: "gemini-2.5-pro",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
},
|
||||
messages: [
|
||||
{ id: "msg-001", sessionId: "session-001", role: "assistant", content: "Hi!", createdAt: "2026-04-08T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const modelTag = document.querySelector(".chat-model-tag");
|
||||
expect(modelTag?.textContent).toContain("Gemini");
|
||||
});
|
||||
|
||||
it("returns null when modelId is missing", () => {
|
||||
setupMockChat({
|
||||
activeSession: {
|
||||
id: "session-001",
|
||||
agentId: "__kb_agent__",
|
||||
status: "active",
|
||||
title: "Test",
|
||||
modelProvider: "anthropic",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
},
|
||||
messages: [
|
||||
{ id: "msg-001", sessionId: "session-001", role: "assistant", content: "Hi!", createdAt: "2026-04-08T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const modelTag = document.querySelector(".chat-model-tag");
|
||||
expect(modelTag).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it("returns null when provider is missing", () => {
|
||||
setupMockChat({
|
||||
activeSession: {
|
||||
id: "session-001",
|
||||
agentId: "__kb_agent__",
|
||||
status: "active",
|
||||
title: "Test",
|
||||
modelId: "claude-sonnet-4-5",
|
||||
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||
},
|
||||
messages: [
|
||||
{ id: "msg-001", sessionId: "session-001", role: "assistant", content: "Hi!", createdAt: "2026-04-08T00:00:00.000Z" },
|
||||
],
|
||||
});
|
||||
|
||||
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||
|
||||
const modelTag = document.querySelector(".chat-model-tag");
|
||||
expect(modelTag).not.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
describe("ChatView CSS — nested flexbox scrolling fix", () => {
|
||||
|
||||
@@ -28424,6 +28424,17 @@ html .column.drag-over * {
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.chat-model-tag {
|
||||
font-size: 11px;
|
||||
padding: 1px 6px;
|
||||
border-radius: 4px;
|
||||
background: var(--surface-bg, rgba(255, 255, 255, 0.08));
|
||||
color: var(--text-secondary);
|
||||
margin-left: 6px;
|
||||
white-space: nowrap;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.chat-message-content {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user