FN-7462: show mobile chat conversation titles
Mobile direct-chat dropdowns now identify sessions by conversation title instead of model-name text. - Render the mobile session trigger with the conversation title or Untitled while preserving the provider logo. - Remove mobile trigger model-tag styling and assert the model badge stays out of the compact header. - Document the mobile Chat dropdown behavior and add a patch changeset for the published CLI package. Files changed: .changeset/fn-7462-mobile-chat-dropdown-title.md | 7 ++++ docs/dashboard-guide.md | 2 +- packages/dashboard/app/components/ChatView.css | 8 ----- packages/dashboard/app/components/ChatView.tsx | 8 +++-- .../__tests__/ChatView.core-contracts.test.tsx | 25 +++++++++++--- .../components/__tests__/ChatView.mobile.test.tsx | 39 +++++++++++++++++++++- 6 files changed, 72 insertions(+), 17 deletions(-) Fusion-Task-Id: FN-7462 Fusion-Task-Lineage: 6a6e2ac3-ecca-4076-81c5-b4e4a65eb286 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7462-mobile-chat-dropdown-title.md
Normal file
7
.changeset/fn-7462-mobile-chat-dropdown-title.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Show conversation titles in the mobile Chat dropdown.
|
||||||
|
category: fix
|
||||||
|
dev: Keeps the provider logo while removing model-name text from the mobile direct-chat trigger.
|
||||||
@@ -412,7 +412,7 @@ Chat view provides project-scoped conversations with agents.
|
|||||||
- If you queue follow-up user messages while the assistant is still streaming, Chat persists them per session, stacks each queued preview above the input box with one shared divider, and restores/sends them one at a time in FIFO order once each active response finishes if you leave and return.
|
- If you queue follow-up user messages while the assistant is still streaming, Chat persists them per session, stacks each queued preview above the input box with one shared divider, and restores/sends them one at a time in FIFO order once each active response finishes if you leave and return.
|
||||||
- Chat message lists now track near-bottom scroll state: while you are reading older messages, live streaming/new replies do not force-scroll; a **Latest** jump control appears until you return to the tail.
|
- Chat message lists now track near-bottom scroll state: while you are reading older messages, live streaming/new replies do not force-scroll; a **Latest** jump control appears until you return to the tail.
|
||||||
- On mobile direct-chat threads, entering a thread and restoring Chat after tab/page visibility returns re-anchors to the newest message (`scrollTop = scrollHeight`) so the view always opens at the live tail.
|
- On mobile direct-chat threads, entering a thread and restoring Chat after tab/page visibility returns re-anchors to the newest message (`scrollTop = scrollHeight`) so the view always opens at the live tail.
|
||||||
- On mobile direct-chat threads, the top Chat header collapses into one compact row: the back button is the far-left visible control and the active conversation dropdown stays beside it, while the visible Chat icon/title shell is hidden to preserve transcript space. Tapping the active conversation opens a lightweight dropdown so you can switch to another direct session or start a New Chat without backing out to the sidebar list first; long conversation titles stay readable in the dropdown via wrapped option text and taller touch-friendly rows.
|
- On mobile direct-chat threads, the top Chat header collapses into one compact row: the back button is the far-left visible control and the active conversation dropdown stays beside it, while the visible Chat icon/title shell is hidden to preserve transcript space. The dropdown trigger shows the conversation title with the provider/model logo only (no model-name text), and tapping it opens a lightweight dropdown so you can switch to another direct session or start a New Chat without backing out to the sidebar list first; long conversation titles stay readable in the dropdown via wrapped option text and taller touch-friendly rows.
|
||||||
- On mobile direct-chat threads, the single thread-wide Markdown/plain eye toggle floats above the transcript/composer area instead of occupying a second header row; desktop/tablet keeps the toggle in the thread header.
|
- On mobile direct-chat threads, the single thread-wide Markdown/plain eye toggle floats above the transcript/composer area instead of occupying a second header row; desktop/tablet keeps the toggle in the thread header.
|
||||||
- Direct chat sessions can be renamed from the sidebar row edit button, the desktop conversation context menu, and the mobile session switcher; blank rename submissions clear the custom title so the default session label is shown again.
|
- Direct chat sessions can be renamed from the sidebar row edit button, the desktop conversation context menu, and the mobile session switcher; blank rename submissions clear the custom title so the default session label is shown again.
|
||||||
<!-- FNXC:ChatViewDocs 2026-07-01-00:00: Task-detail planner chats are intentionally hidden from the common Direct feed by default after issue #1850; Settings keeps an opt-in for operators who want populated task-planner sessions restored without adding a mandatory Tasks tab. -->
|
<!-- FNXC:ChatViewDocs 2026-07-01-00:00: Task-detail planner chats are intentionally hidden from the common Direct feed by default after issue #1850; Settings keeps an opt-in for operators who want populated task-planner sessions restored without adding a mandatory Tasks tab. -->
|
||||||
|
|||||||
@@ -615,14 +615,6 @@ The Direct-chat context budget indicator is a secondary desktop/tablet header af
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-mobile-session-trigger .chat-model-tag {
|
|
||||||
flex-shrink: 0;
|
|
||||||
max-width: calc(var(--space-xl) * 4);
|
|
||||||
overflow: hidden;
|
|
||||||
text-overflow: ellipsis;
|
|
||||||
white-space: nowrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-mobile-session-trigger .provider-icon,
|
.chat-mobile-session-trigger .provider-icon,
|
||||||
.chat-mobile-session-trigger > svg:first-child {
|
.chat-mobile-session-trigger > svg:first-child {
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
|
|||||||
@@ -2139,6 +2139,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
|||||||
const threadHeaderTitle = activeSession?.agentId === FN_AGENT_ID
|
const threadHeaderTitle = activeSession?.agentId === FN_AGENT_ID
|
||||||
? (activeModelTag ?? "Fusion")
|
? (activeModelTag ?? "Fusion")
|
||||||
: activeSession?.title || agentsMap.get(activeSession?.agentId ?? "")?.name || activeSession?.agentId || "Chat";
|
: activeSession?.title || agentsMap.get(activeSession?.agentId ?? "")?.name || activeSession?.agentId || "Chat";
|
||||||
|
const mobileDirectSessionTitle = activeSession?.title || t("chat.untitledSession", "Untitled");
|
||||||
|
|
||||||
const showThreadHeaderModelTag = Boolean(activeModelTag && activeModelTag !== threadHeaderTitle);
|
const showThreadHeaderModelTag = Boolean(activeModelTag && activeModelTag !== threadHeaderTitle);
|
||||||
const showThreadHeaderContextWindow = !isChatMobile && hasThreadInView && activeContextWindow !== null;
|
const showThreadHeaderContextWindow = !isChatMobile && hasThreadInView && activeContextWindow !== null;
|
||||||
@@ -2590,8 +2591,11 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
|||||||
onClick={() => setMobileSessionMenuOpen((open) => !open)}
|
onClick={() => setMobileSessionMenuOpen((open) => !open)}
|
||||||
>
|
>
|
||||||
{activeModelProvider ? <ProviderIcon provider={activeModelProvider} size="md" /> : <Bot size={16} />}
|
{activeModelProvider ? <ProviderIcon provider={activeModelProvider} size="md" /> : <Bot size={16} />}
|
||||||
<span className="chat-thread-header-title">{threadHeaderTitle}</span>
|
{/*
|
||||||
{showThreadHeaderModelTag && <span className="chat-model-tag">{activeModelTag}</span>}
|
FNXC:ChatHeader 2026-07-03-00:00:
|
||||||
|
Mobile direct-chat needs one compact, conversation-oriented dropdown trigger. Show the active conversation title (or Untitled) beside only the provider/model logo and chevron; keep model-name badges exclusive to the desktop/thread identity row.
|
||||||
|
*/}
|
||||||
|
<span className="chat-thread-header-title">{mobileDirectSessionTitle}</span>
|
||||||
<ChevronDown size={16} aria-hidden="true" />
|
<ChevronDown size={16} aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
{mobileSessionMenuOpen && (
|
{mobileSessionMenuOpen && (
|
||||||
|
|||||||
@@ -346,16 +346,27 @@ describe("Chat Session Delete Button", () => {
|
|||||||
const restoreMatchMedia = mockViewportMode("mobile");
|
const restoreMatchMedia = mockViewportMode("mobile");
|
||||||
const renameSession = vi.fn().mockResolvedValue(undefined);
|
const renameSession = vi.fn().mockResolvedValue(undefined);
|
||||||
try {
|
try {
|
||||||
|
const initialSession: ChatSessionInfo = {
|
||||||
|
id: "session-001",
|
||||||
|
agentId: "__fn_agent__",
|
||||||
|
status: "active",
|
||||||
|
title: "Mobile Chat",
|
||||||
|
modelProvider: "minimax",
|
||||||
|
modelId: "m3",
|
||||||
|
createdAt: "2026-04-08T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||||
|
};
|
||||||
setupMockChat({
|
setupMockChat({
|
||||||
activeSession: { id: "session-001", agentId: "agent-001", status: "active", title: "Mobile Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" },
|
activeSession: initialSession,
|
||||||
sessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Mobile Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
sessions: [initialSession],
|
||||||
filteredSessions: [{ id: "session-001", agentId: "agent-001", status: "active", title: "Mobile Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" }],
|
filteredSessions: [initialSession],
|
||||||
renameSession,
|
renameSession,
|
||||||
});
|
});
|
||||||
|
|
||||||
const view = await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
const view = await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||||
|
|
||||||
expect(screen.getByTestId("chat-mobile-session-trigger")).toHaveTextContent("Mobile Chat");
|
expect(screen.getByTestId("chat-mobile-session-trigger")).toHaveTextContent("Mobile Chat");
|
||||||
|
expect(screen.getByTestId("chat-mobile-session-trigger")).not.toHaveTextContent("M3");
|
||||||
await userEvent.click(screen.getByTestId("chat-mobile-session-trigger"));
|
await userEvent.click(screen.getByTestId("chat-mobile-session-trigger"));
|
||||||
await userEvent.click(screen.getByTestId("chat-mobile-session-rename-session-001"));
|
await userEvent.click(screen.getByTestId("chat-mobile-session-rename-session-001"));
|
||||||
|
|
||||||
@@ -367,7 +378,7 @@ describe("Chat Session Delete Button", () => {
|
|||||||
|
|
||||||
expect(renameSession).toHaveBeenCalledWith("session-001", "Mobile Renamed");
|
expect(renameSession).toHaveBeenCalledWith("session-001", "Mobile Renamed");
|
||||||
|
|
||||||
const renamedSession: ChatSessionInfo = { id: "session-001", agentId: "agent-001", status: "active", title: "Mobile Renamed", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" };
|
const renamedSession: ChatSessionInfo = { ...initialSession, title: "Mobile Renamed" };
|
||||||
setupMockChat({
|
setupMockChat({
|
||||||
activeSession: renamedSession,
|
activeSession: renamedSession,
|
||||||
sessions: [renamedSession],
|
sessions: [renamedSession],
|
||||||
@@ -378,7 +389,10 @@ describe("Chat Session Delete Button", () => {
|
|||||||
view.rerender(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
view.rerender(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(screen.getByTestId("chat-mobile-session-trigger")).toHaveTextContent("Mobile Renamed");
|
const trigger = screen.getByTestId("chat-mobile-session-trigger");
|
||||||
|
expect(trigger).toHaveTextContent("Mobile Renamed");
|
||||||
|
expect(trigger).not.toHaveTextContent("M3");
|
||||||
|
expect(trigger.querySelector(".chat-model-tag")).not.toBeInTheDocument();
|
||||||
const headerTitle = document.querySelector(".chat-thread-header-title") as HTMLElement | null;
|
const headerTitle = document.querySelector(".chat-thread-header-title") as HTMLElement | null;
|
||||||
expect(headerTitle).toHaveTextContent("Mobile Renamed");
|
expect(headerTitle).toHaveTextContent("Mobile Renamed");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -606,6 +620,7 @@ describe("ChatView CSS — mobile thread switcher", () => {
|
|||||||
expect(optionTitleMatch?.[1]).toContain("line-height: normal");
|
expect(optionTitleMatch?.[1]).toContain("line-height: normal");
|
||||||
expect(optionTitleMatch?.[1]).toContain("white-space: normal");
|
expect(optionTitleMatch?.[1]).toContain("white-space: normal");
|
||||||
expect(optionTitleMatch?.[1]).toContain("overflow-wrap: anywhere");
|
expect(optionTitleMatch?.[1]).toContain("overflow-wrap: anywhere");
|
||||||
|
expect(css).not.toMatch(/\.chat-mobile-session-trigger\s+\.chat-model-tag/);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("keeps mobile override for header identity overflow visible so dropdown can render", async () => {
|
it("keeps mobile override for header identity overflow visible so dropdown can render", async () => {
|
||||||
|
|||||||
@@ -268,12 +268,48 @@ describe("ChatView mobile behavior", () => {
|
|||||||
expect(backButton.compareDocumentPosition(sessionTrigger) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
expect(backButton.compareDocumentPosition(sessionTrigger) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||||
expect(headerTitle).not.toContainElement(backButton);
|
expect(headerTitle).not.toContainElement(backButton);
|
||||||
expect(headerTitle.querySelector("svg")).toHaveAttribute("aria-hidden", "true");
|
expect(headerTitle.querySelector("svg")).toHaveAttribute("aria-hidden", "true");
|
||||||
expect(sessionTrigger).toHaveTextContent("M3");
|
expect(sessionTrigger).toHaveTextContent("Testing");
|
||||||
|
expect(sessionTrigger).not.toHaveTextContent("M3");
|
||||||
|
expect(sessionTrigger).not.toHaveTextContent("MiniMax M3");
|
||||||
|
expect(within(sessionTrigger).getByTestId("minimax-icon")).toBeInTheDocument();
|
||||||
|
expect(sessionTrigger.querySelector(".chat-model-tag")).not.toBeInTheDocument();
|
||||||
expect(viewHeader).not.toContainElement(renderToggle);
|
expect(viewHeader).not.toContainElement(renderToggle);
|
||||||
expect(renderToggle).toHaveClass("chat-thread-header-render-toggle--floating");
|
expect(renderToggle).toHaveClass("chat-thread-header-render-toggle--floating");
|
||||||
expect(screen.getAllByTestId("chat-back-btn")).toHaveLength(1);
|
expect(screen.getAllByTestId("chat-back-btn")).toHaveLength(1);
|
||||||
expect(screen.getAllByTestId("chat-mobile-session-trigger")).toHaveLength(1);
|
expect(screen.getAllByTestId("chat-mobile-session-trigger")).toHaveLength(1);
|
||||||
expect(screen.getByRole("heading", { name: "Chat" })).toBeInTheDocument();
|
expect(screen.getByRole("heading", { name: "Chat" })).toBeInTheDocument();
|
||||||
|
expect(document.querySelector(".chat-thread-header")).not.toBeInTheDocument();
|
||||||
|
} finally {
|
||||||
|
restoreMatchMedia.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mobile mode: model-only empty title uses Untitled without model-name text", async () => {
|
||||||
|
const restoreMatchMedia = mockMobileViewport();
|
||||||
|
try {
|
||||||
|
setupMockChat({
|
||||||
|
activeSession: {
|
||||||
|
id: "session-001",
|
||||||
|
agentId: "__fn_agent__",
|
||||||
|
status: "active",
|
||||||
|
title: null,
|
||||||
|
modelProvider: "minimax",
|
||||||
|
modelId: "m3",
|
||||||
|
createdAt: "2026-04-08T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
messages: [{ id: "msg-001", sessionId: "session-001", role: "assistant", content: "Hello", createdAt: "2026-04-08T00:00:00.000Z" }],
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
const trigger = screen.getByTestId("chat-mobile-session-trigger");
|
||||||
|
expect(trigger).toHaveTextContent("Untitled");
|
||||||
|
expect(trigger).not.toHaveTextContent("M3");
|
||||||
|
expect(trigger).not.toHaveTextContent("MiniMax M3");
|
||||||
|
expect(within(trigger).getByTestId("minimax-icon")).toBeInTheDocument();
|
||||||
|
expect(trigger.querySelector(".chat-model-tag")).not.toBeInTheDocument();
|
||||||
|
expect(screen.getAllByTestId("chat-mobile-session-trigger")).toHaveLength(1);
|
||||||
} finally {
|
} finally {
|
||||||
restoreMatchMedia.mockRestore();
|
restoreMatchMedia.mockRestore();
|
||||||
}
|
}
|
||||||
@@ -429,6 +465,7 @@ describe("ChatView mobile behavior", () => {
|
|||||||
expect(headerActions.firstElementChild).toBe(backButton);
|
expect(headerActions.firstElementChild).toBe(backButton);
|
||||||
expect(trigger).toHaveTextContent(longTitle);
|
expect(trigger).toHaveTextContent(longTitle);
|
||||||
expect(within(trigger).getByTestId("icon-bot")).toBeInTheDocument();
|
expect(within(trigger).getByTestId("icon-bot")).toBeInTheDocument();
|
||||||
|
expect(trigger.querySelector(".chat-model-tag")).not.toBeInTheDocument();
|
||||||
expect(screen.getAllByTestId("chat-mobile-session-trigger")).toHaveLength(1);
|
expect(screen.getAllByTestId("chat-mobile-session-trigger")).toHaveLength(1);
|
||||||
|
|
||||||
await userEvent.click(trigger);
|
await userEvent.click(trigger);
|
||||||
|
|||||||
Reference in New Issue
Block a user