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.
|
||||
- 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, 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.
|
||||
- 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. -->
|
||||
|
||||
@@ -615,14 +615,6 @@ The Direct-chat context budget indicator is a secondary desktop/tablet header af
|
||||
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 > svg:first-child {
|
||||
flex-shrink: 0;
|
||||
|
||||
@@ -2139,6 +2139,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
const threadHeaderTitle = activeSession?.agentId === FN_AGENT_ID
|
||||
? (activeModelTag ?? "Fusion")
|
||||
: 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 showThreadHeaderContextWindow = !isChatMobile && hasThreadInView && activeContextWindow !== null;
|
||||
@@ -2590,8 +2591,11 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
||||
onClick={() => setMobileSessionMenuOpen((open) => !open)}
|
||||
>
|
||||
{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" />
|
||||
</button>
|
||||
{mobileSessionMenuOpen && (
|
||||
|
||||
@@ -346,16 +346,27 @@ describe("Chat Session Delete Button", () => {
|
||||
const restoreMatchMedia = mockViewportMode("mobile");
|
||||
const renameSession = vi.fn().mockResolvedValue(undefined);
|
||||
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({
|
||||
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" },
|
||||
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" }],
|
||||
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" }],
|
||||
activeSession: initialSession,
|
||||
sessions: [initialSession],
|
||||
filteredSessions: [initialSession],
|
||||
renameSession,
|
||||
});
|
||||
|
||||
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")).not.toHaveTextContent("M3");
|
||||
await userEvent.click(screen.getByTestId("chat-mobile-session-trigger"));
|
||||
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");
|
||||
|
||||
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({
|
||||
activeSession: renamedSession,
|
||||
sessions: [renamedSession],
|
||||
@@ -378,7 +389,10 @@ describe("Chat Session Delete Button", () => {
|
||||
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;
|
||||
expect(headerTitle).toHaveTextContent("Mobile Renamed");
|
||||
} finally {
|
||||
@@ -606,6 +620,7 @@ describe("ChatView CSS — mobile thread switcher", () => {
|
||||
expect(optionTitleMatch?.[1]).toContain("line-height: normal");
|
||||
expect(optionTitleMatch?.[1]).toContain("white-space: normal");
|
||||
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 () => {
|
||||
|
||||
@@ -268,12 +268,48 @@ describe("ChatView mobile behavior", () => {
|
||||
expect(backButton.compareDocumentPosition(sessionTrigger) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(headerTitle).not.toContainElement(backButton);
|
||||
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(renderToggle).toHaveClass("chat-thread-header-render-toggle--floating");
|
||||
expect(screen.getAllByTestId("chat-back-btn")).toHaveLength(1);
|
||||
expect(screen.getAllByTestId("chat-mobile-session-trigger")).toHaveLength(1);
|
||||
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 {
|
||||
restoreMatchMedia.mockRestore();
|
||||
}
|
||||
@@ -429,6 +465,7 @@ describe("ChatView mobile behavior", () => {
|
||||
expect(headerActions.firstElementChild).toBe(backButton);
|
||||
expect(trigger).toHaveTextContent(longTitle);
|
||||
expect(within(trigger).getByTestId("icon-bot")).toBeInTheDocument();
|
||||
expect(trigger.querySelector(".chat-model-tag")).not.toBeInTheDocument();
|
||||
expect(screen.getAllByTestId("chat-mobile-session-trigger")).toHaveLength(1);
|
||||
|
||||
await userEvent.click(trigger);
|
||||
|
||||
Reference in New Issue
Block a user