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:
gsxdsm
2026-07-03 00:44:21 -07:00
parent 30a11ac919
commit d9ef51492d
6 changed files with 72 additions and 17 deletions

View 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.

View File

@@ -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. -->

View File

@@ -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;

View File

@@ -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 && (

View File

@@ -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 () => {

View File

@@ -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);