feat(FN-2195): unify chat model labeling for fn assistant sessions

- Show formatted model names in sidebar metadata for fn assistant sessions with a Fusion fallback
- Use the resolved model name as the thread header title and assistant avatar label for fn sessions
- Suppress duplicate model tags when the primary label already displays the model while keeping tags for non-fn sessions
- Expand ChatView tests to cover sidebar, header, and avatar model-label and fallback behavior
This commit is contained in:
Fusion
2026-04-19 23:36:11 -07:00
committed by gsxdsm
parent e8b0ed5627
commit eaf99e6279
2 changed files with 164 additions and 41 deletions

View File

@@ -868,12 +868,22 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
);
};
const activeModelTag = formatModelTag(activeSession?.modelProvider, activeSession?.modelId);
const threadHeaderTitle = activeSession?.agentId === FN_AGENT_ID
? (activeModelTag ?? "Fusion")
: activeSession?.title || agentsMap.get(activeSession?.agentId ?? "")?.name || activeSession?.agentId || "Chat";
const showThreadHeaderModelTag = Boolean(activeModelTag && activeModelTag !== threadHeaderTitle);
const agentName =
agentsMap.get(activeSession?.agentId ?? "")?.name ||
(activeSession?.agentId === FN_AGENT_ID
? "Fusion"
? (activeModelTag ?? "Fusion")
: (activeSession?.agentId?.slice(0, 30) ?? "Fusion"));
const showAssistantModelTag = Boolean(activeModelTag && activeModelTag !== agentName);
const pendingPreview = pendingMessage.length > 50
? `${pendingMessage.slice(0, 50)}`
: pendingMessage;
@@ -942,7 +952,12 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
{session.lastMessagePreview || "No messages"}
</div>
<div className="chat-session-meta">
<span>{agentsMap.get(session.agentId)?.name || (session.agentId === FN_AGENT_ID ? "Fusion" : session.agentId.slice(0, 30))}</span>
<span>
{agentsMap.get(session.agentId)?.name ||
(session.agentId === FN_AGENT_ID
? (formatModelTag(session.modelProvider, session.modelId) ?? "Fusion")
: session.agentId.slice(0, 30))}
</span>
<span>{session.updatedAt ? formatRelativeTime(session.updatedAt) : ""}</span>
</div>
</div>
@@ -1011,15 +1026,8 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
</button>
)}
<Bot size={16} />
<span className="chat-thread-header-title">
{activeSession?.agentId === FN_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;
})()}
<span className="chat-thread-header-title">{threadHeaderTitle}</span>
{showThreadHeaderModelTag && <span className="chat-model-tag">{activeModelTag}</span>}
</div>
{/* Messages */}
@@ -1044,10 +1052,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
<div className="chat-message-avatar">
<Bot size={14} />
<span>{agentName}</span>
{activeSession && (() => {
const modelTag = formatModelTag(activeSession.modelProvider, activeSession.modelId);
return modelTag ? <span className="chat-model-tag">{modelTag}</span> : null;
})()}
{showAssistantModelTag && <span className="chat-model-tag">{activeModelTag}</span>}
</div>
)}
<div className="chat-message-content">{renderMessageContent(message.content)}</div>
@@ -1065,10 +1070,7 @@ export function ChatView({ projectId, addToast }: ChatViewProps) {
<div className="chat-message-avatar">
<Bot size={14} />
<span>{agentName}</span>
{activeSession && (() => {
const modelTag = formatModelTag(activeSession.modelProvider, activeSession.modelId);
return modelTag ? <span className="chat-model-tag">{modelTag}</span> : null;
})()}
{showAssistantModelTag && <span className="chat-model-tag">{activeModelTag}</span>}
</div>
{streamingText ? (
<div className="chat-message-content">{renderMessageContent(streamingText)}</div>

View File

@@ -432,6 +432,34 @@ describe("ChatView", () => {
expect(within(avatar!).getByText("Fusion")).toBeInTheDocument();
});
it("shows formatted model name in assistant message avatar for fn agent sessions", async () => {
setupMockChat({
activeSession: {
id: "session-001",
agentId: "__fn_agent__",
status: "active",
title: "Fusion Chat",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
updatedAt: "2026-04-08T00:00:00.000Z",
},
messages: [
{ id: "msg-001", sessionId: "session-001", role: "assistant", content: "Built-in assistant response", createdAt: "2026-04-08T00:00:00.000Z" },
],
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const avatar = document.querySelector(".chat-message-avatar");
expect(avatar).toBeInTheDocument();
await waitFor(() => {
expect(within(avatar!).getByText("Claude Sonnet 4.5")).toBeInTheDocument();
});
expect(within(avatar!).queryByText("Fusion")).not.toBeInTheDocument();
expect(avatar?.querySelector(".chat-model-tag")).toBeNull();
});
it("shows resolved agent name in streaming assistant avatar", async () => {
setupMockChat({
activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Agent Chat", updatedAt: "2026-04-08T00:00:00.000Z" },
@@ -1002,7 +1030,36 @@ describe("ChatView", () => {
expect(within(dialog!).getByText("Delete Conversation?")).toBeInTheDocument();
});
it("shows Fusion label for fn agent sessions in sidebar", () => {
it("shows formatted model label for fn agent sessions in sidebar", () => {
setupMockChat({
sessions: [{
id: "session-001",
agentId: "__fn_agent__",
status: "active",
title: "My Chat",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
updatedAt: "2026-04-08T00:00:00.000Z",
}],
filteredSessions: [{
id: "session-001",
agentId: "__fn_agent__",
status: "active",
title: "My Chat",
modelProvider: "anthropic",
modelId: "claude-sonnet-4-5",
updatedAt: "2026-04-08T00:00:00.000Z",
}],
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const sessionItem = screen.getByTestId("chat-session-session-001");
expect(within(sessionItem).getByText("Claude Sonnet 4.5")).toBeInTheDocument();
expect(within(sessionItem).queryByText("Fusion")).not.toBeInTheDocument();
});
it("shows Fusion fallback for fn agent sessions in sidebar without model info", () => {
setupMockChat({
sessions: [{ id: "session-001", agentId: "__fn_agent__", status: "active", title: "My Chat", updatedAt: "2026-04-08T00:00:00.000Z" }],
filteredSessions: [{ id: "session-001", agentId: "__fn_agent__", status: "active", title: "My Chat", updatedAt: "2026-04-08T00:00:00.000Z" }],
@@ -1011,7 +1068,6 @@ describe("ChatView", () => {
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const sessionItem = screen.getByTestId("chat-session-session-001");
// Should show "Fusion" instead of "__fn_agent__"
expect(within(sessionItem).getByText("Fusion")).toBeInTheDocument();
});
@@ -1028,7 +1084,7 @@ describe("ChatView", () => {
expect(within(sessionItem).getByText("my-custom-agent")).toBeInTheDocument();
});
it("shows model tag in thread header when session has model", () => {
it("shows formatted model name in thread header title for fn agent sessions", () => {
setupMockChat({
activeSession: {
id: "session-001",
@@ -1039,6 +1095,30 @@ describe("ChatView", () => {
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 title = document.querySelector(".chat-thread-header-title");
expect(title).toBeInTheDocument();
expect(title).toHaveTextContent("Claude Sonnet 4.5");
expect(title).not.toHaveTextContent("Fusion");
});
it("shows model tag in thread header when non-fn session has model", () => {
setupMockChat({
activeSession: {
id: "session-001",
agentId: "agent-001",
status: "active",
title: "Agent 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" },
@@ -1047,9 +1127,34 @@ describe("ChatView", () => {
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const modelTag = document.querySelector(".chat-model-tag");
expect(modelTag).toBeInTheDocument();
expect(modelTag?.textContent).toContain("Claude");
const headerModelTag = document.querySelector(".chat-thread-header .chat-model-tag");
expect(headerModelTag).toBeInTheDocument();
expect(headerModelTag?.textContent).toContain("Claude");
});
it("does not show duplicate model tag in thread header for fn agent sessions", () => {
setupMockChat({
activeSession: {
id: "session-001",
agentId: "__fn_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: "assistant", content: "Hi!", createdAt: "2026-04-08T00:00:00.000Z" },
],
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const title = document.querySelector(".chat-thread-header-title");
expect(title).toHaveTextContent("Claude Sonnet 4.5");
const headerModelTag = document.querySelector(".chat-thread-header .chat-model-tag");
expect(headerModelTag).toBeNull();
});
it("does not show model tag when session has no model", () => {
@@ -1073,7 +1178,30 @@ describe("ChatView", () => {
expect(modelTag).not.toBeInTheDocument();
});
it("shows model tag in message avatar when session has model", () => {
it("shows model tag in message avatar when non-fn session has model", () => {
setupMockChat({
activeSession: {
id: "session-001",
agentId: "agent-001",
status: "active",
title: "Agent Chat",
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:01:00.000Z" },
],
});
render(<ChatView projectId="proj-123" addToast={vi.fn()} />);
const avatar = document.querySelector(".chat-message-avatar");
expect(avatar).toBeInTheDocument();
expect(avatar?.querySelector(".chat-model-tag")?.textContent).toContain("GPT");
});
it("does not show duplicate model tag in message avatar for fn agent sessions", () => {
setupMockChat({
activeSession: {
id: "session-001",
@@ -1085,23 +1213,16 @@ describe("ChatView", () => {
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" },
{ id: "msg-001", 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");
const avatar = document.querySelector(".chat-message-avatar");
expect(avatar).toBeInTheDocument();
expect(within(avatar!).getByText("GPT-4o")).toBeInTheDocument();
expect(avatar?.querySelector(".chat-model-tag")).toBeNull();
});
});
@@ -1113,7 +1234,7 @@ describe("formatModelTag helper function", () => {
setupMockChat({
activeSession: {
id: "session-001",
agentId: "__fn_agent__",
agentId: "agent-001",
status: "active",
title: "Test",
modelProvider: "anthropic",
@@ -1135,7 +1256,7 @@ describe("formatModelTag helper function", () => {
setupMockChat({
activeSession: {
id: "session-001",
agentId: "__fn_agent__",
agentId: "agent-001",
status: "active",
title: "Test",
modelProvider: "openai",
@@ -1157,7 +1278,7 @@ describe("formatModelTag helper function", () => {
setupMockChat({
activeSession: {
id: "session-001",
agentId: "__fn_agent__",
agentId: "agent-001",
status: "active",
title: "Test",
modelProvider: "google",