FN-7441: add sidebar rename controls for chat conversations
Add an explicit sidebar rename affordance for direct Chat conversations while preserving existing rename paths. - Add edit buttons beside delete controls on direct chat session rows and wire them to the existing rename dialog. - Keep sidebar action spacing and accessibility labels stable across desktop and mobile layouts. - Cover sidebar, context-menu, and mobile switcher rename behavior with ChatView tests. - Document the sidebar rename entry point and add the published package changeset. Files changed: .changeset/fn-7441-chat-conversation-rename.md | 7 ++ docs/dashboard-guide.md | 2 +- packages/dashboard/app/components/ChatView.css | 51 ++++----- packages/dashboard/app/components/ChatView.tsx | 54 ++++++--- .../__tests__/ChatView.core-interactions.test.tsx | 122 +++++++++++++++++++++ .../components/__tests__/ChatView.mobile.test.tsx | 72 ++++++++++++ 6 files changed, 264 insertions(+), 44 deletions(-) Fusion-Task-Id: FN-7441 Fusion-Task-Lineage: 39a2c393-dd85-4362-9472-58bb137b906c Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7441-chat-conversation-rename.md
Normal file
7
.changeset/fn-7441-chat-conversation-rename.md
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
---
|
||||||
|
"@runfusion/fusion": patch
|
||||||
|
---
|
||||||
|
|
||||||
|
summary: Add sidebar rename buttons to direct Chat conversations.
|
||||||
|
category: feature
|
||||||
|
dev: Reuses ChatView's existing rename dialog and useChat renameSession path.
|
||||||
@@ -408,7 +408,7 @@ Chat view provides project-scoped conversations with agents.
|
|||||||
- 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, tapping the active title/identity in the thread header opens a lightweight conversation 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 now stay readable in the dropdown via wrapped option text and taller touch-friendly rows.
|
- On mobile direct-chat threads, tapping the active title/identity in the thread header opens a lightweight conversation 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 now stay readable in the dropdown via wrapped option text and taller touch-friendly rows.
|
||||||
- Direct chat sessions can be renamed from the desktop conversation context menu and from 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. -->
|
||||||
<!-- FNXC:TaskDetailPlannerChat 2026-07-01-22:02: Done-task planner Chat remains available for retrospective Q&A and can create a task-scoped refinement through the planner tool, while common Chat feed visibility remains opt-in. -->
|
<!-- FNXC:TaskDetailPlannerChat 2026-07-01-22:02: Done-task planner Chat remains available for retrospective Q&A and can create a task-scoped refinement through the planner tool, while common Chat feed visibility remains opt-in. -->
|
||||||
- Task-detail planner Chat conversations stay available from each task's **Chat** tab, including after the task is `done`. They are hidden from the common Direct/common Chat feed by default; enable **Settings → Project General → Show task chats in common Chat feed** to include populated task chats again. Empty task chat sessions stay hidden either way. Planner Chat can answer token-count, estimated-cost, runtime, timing-event, workflow-step duration, and per-model usage questions for the current task through a read-only task-scoped metrics tool; unknown/stale pricing is reported as uncertain instead of `$0`. On completed tasks, clear follow-up implementation or improvement requests can create a normal refinement task from the completed source task.
|
- Task-detail planner Chat conversations stay available from each task's **Chat** tab, including after the task is `done`. They are hidden from the common Direct/common Chat feed by default; enable **Settings → Project General → Show task chats in common Chat feed** to include populated task chats again. Empty task chat sessions stay hidden either way. Planner Chat can answer token-count, estimated-cost, runtime, timing-event, workflow-step duration, and per-model usage questions for the current task through a read-only task-scoped metrics tool; unknown/stale pricing is reported as uncertain instead of `$0`. On completed tasks, clear follow-up implementation or improvement requests can create a normal refinement task from the completed source task.
|
||||||
|
|||||||
@@ -344,7 +344,7 @@ When the movable chat popup is resized narrow, collapse Direct/Rooms labels to i
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
padding-right: calc(var(--space-md) * 3);
|
padding-right: calc((var(--space-md) * 4) + (var(--space-xs) * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-session-preview {
|
.chat-session-preview {
|
||||||
@@ -353,7 +353,7 @@ When the movable chat popup is resized narrow, collapse Direct/Rooms labels to i
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
text-overflow: ellipsis;
|
||||||
padding-right: calc(var(--space-md) * 3);
|
padding-right: calc((var(--space-md) * 4) + (var(--space-xs) * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-session-meta {
|
.chat-session-meta {
|
||||||
@@ -378,39 +378,37 @@ When the movable chat popup is resized narrow, collapse Direct/Rooms labels to i
|
|||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Chat Session Delete Button === */
|
/*
|
||||||
.chat-session-delete-btn {
|
FNXC:ChatSidebar 2026-07-02-00:00:
|
||||||
|
Direct conversation rows expose edit and delete as a compact action pair. Keep the pair outside the row text flow so title/preview truncation stays stable across desktop, mobile, and compact right-dock layouts.
|
||||||
|
*/
|
||||||
|
.chat-session-actions {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: var(--space-sm);
|
top: var(--space-sm);
|
||||||
right: var(--space-sm);
|
right: var(--space-sm);
|
||||||
display: flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
gap: var(--space-xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.chat-session-action-btn {
|
||||||
width: calc(var(--space-md) * 2);
|
width: calc(var(--space-md) * 2);
|
||||||
|
min-width: calc(var(--space-md) * 2);
|
||||||
height: calc(var(--space-md) * 2);
|
height: calc(var(--space-md) * 2);
|
||||||
padding: 0;
|
min-height: calc(var(--space-md) * 2);
|
||||||
background: transparent;
|
flex: 0 0 auto;
|
||||||
border: none;
|
|
||||||
border-radius: var(--radius-sm);
|
border-radius: var(--radius-sm);
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
cursor: pointer;
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity var(--transition-fast), color var(--transition-fast);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-session-item:hover .chat-session-delete-btn {
|
.chat-session-action-btn:hover,
|
||||||
opacity: 1;
|
.chat-session-action-btn:focus-visible {
|
||||||
|
color: var(--text);
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-session-delete-btn:hover {
|
.chat-session-delete-btn:hover,
|
||||||
|
.chat-session-delete-btn:focus-visible {
|
||||||
color: var(--color-error);
|
color: var(--color-error);
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.chat-session-delete-btn:focus {
|
|
||||||
opacity: 1;
|
|
||||||
outline: none;
|
|
||||||
box-shadow: var(--focus-ring-strong);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Context menu for session items */
|
/* Context menu for session items */
|
||||||
@@ -2200,15 +2198,10 @@ Queued-message banners stack above the composer input with a capped scroll area,
|
|||||||
min-height: calc(var(--space-lg) * 2.25);
|
min-height: calc(var(--space-lg) * 2.25);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* FN-4352: secondary control inside tappable session row; keep compact size (inflated by FN-3628). */
|
/* FN-4352/FN-4385/FN-7441: direct session row action pair stays visible and text keeps clearance for both edit and delete touch targets. */
|
||||||
.chat-session-delete-btn {
|
|
||||||
opacity: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* FN-4352/FN-4385: keep row text clearance aligned to compact secondary delete control (matches base compact clearance). */
|
|
||||||
.chat-session-title,
|
.chat-session-title,
|
||||||
.chat-session-preview {
|
.chat-session-preview {
|
||||||
padding-right: calc(var(--space-md) * 3);
|
padding-right: calc((var(--space-md) * 4) + (var(--space-xs) * 2));
|
||||||
}
|
}
|
||||||
|
|
||||||
.chat-message--assistant .chat-message-render-toggle {
|
.chat-message--assistant .chat-message-render-toggle {
|
||||||
|
|||||||
@@ -2716,6 +2716,7 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
|||||||
defaultModel,
|
defaultModel,
|
||||||
);
|
);
|
||||||
const sessionModelTag = formatModelTag(sessionResolvedModel?.provider, sessionResolvedModel?.modelId) ?? "Fusion";
|
const sessionModelTag = formatModelTag(sessionResolvedModel?.provider, sessionResolvedModel?.modelId) ?? "Fusion";
|
||||||
|
const sessionTitle = session.title || t("chat.untitledSession", "Untitled");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -2728,19 +2729,38 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
|||||||
}}
|
}}
|
||||||
data-testid={`chat-session-${session.id}`}
|
data-testid={`chat-session-${session.id}`}
|
||||||
>
|
>
|
||||||
<button
|
{/*
|
||||||
className="chat-session-delete-btn"
|
FNXC:ChatSidebar 2026-07-02-00:00:
|
||||||
onClick={(e) => {
|
Direct conversation rows need an always-discoverable rename affordance before delete while preserving row selection. Keep edit/delete as sibling buttons that stop propagation and share the existing rename/delete flows.
|
||||||
e.stopPropagation();
|
*/}
|
||||||
setConfirmDelete(session.id);
|
<div className="chat-session-actions">
|
||||||
}}
|
<button
|
||||||
data-testid="chat-session-delete-btn"
|
type="button"
|
||||||
aria-label={t("chat.deleteConversation", "Delete conversation")}
|
className="btn-icon chat-session-action-btn chat-session-rename-btn"
|
||||||
>
|
onClick={(e) => {
|
||||||
<Trash2 size={14} />
|
e.stopPropagation();
|
||||||
</button>
|
openRenameDialog(session.id);
|
||||||
|
}}
|
||||||
|
data-testid="chat-session-rename-btn"
|
||||||
|
aria-label={t("chat.renameConversationAria", "Rename conversation {{title}}", { title: sessionTitle })}
|
||||||
|
>
|
||||||
|
<Pencil size={14} />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn-icon chat-session-action-btn chat-session-delete-btn"
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
setConfirmDelete(session.id);
|
||||||
|
}}
|
||||||
|
data-testid="chat-session-delete-btn"
|
||||||
|
aria-label={t("chat.deleteConversation", "Delete conversation")}
|
||||||
|
>
|
||||||
|
<Trash2 size={14} />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<div className="chat-session-title">
|
<div className="chat-session-title">
|
||||||
{session.title || t("chat.untitledSession", "Untitled")}
|
{sessionTitle}
|
||||||
{showUnreadDot ? (
|
{showUnreadDot ? (
|
||||||
<span
|
<span
|
||||||
className="chat-unread-dot"
|
className="chat-unread-dot"
|
||||||
@@ -2935,8 +2955,14 @@ export function ChatView({ projectId, addToast, floating = false, compactLayout
|
|||||||
{/* Rename Dialog */}
|
{/* Rename Dialog */}
|
||||||
{renameDialog && (
|
{renameDialog && (
|
||||||
<div className="chat-new-dialog-backdrop chat-view-dialog-backdrop" onClick={() => setRenameDialog(null)}>
|
<div className="chat-new-dialog-backdrop chat-view-dialog-backdrop" onClick={() => setRenameDialog(null)}>
|
||||||
<div className="chat-new-dialog chat-view-dialog" onClick={(e) => e.stopPropagation()}>
|
<div
|
||||||
<h3>{t("chat.renameConversationTitle", "Rename Conversation")}</h3>
|
className="chat-new-dialog chat-view-dialog"
|
||||||
|
role="dialog"
|
||||||
|
aria-modal="true"
|
||||||
|
aria-labelledby="chat-rename-dialog-title"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<h3 id="chat-rename-dialog-title">{t("chat.renameConversationTitle", "Rename Conversation")}</h3>
|
||||||
<p className="chat-view-delete-dialog-copy">
|
<p className="chat-view-delete-dialog-copy">
|
||||||
{t("chat.renameConversationBody", "Choose a new name for this conversation. Leave it blank to show Untitled.")}
|
{t("chat.renameConversationBody", "Choose a new name for this conversation. Leave it blank to show Untitled.")}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1288,4 +1288,126 @@ describe("ChatView core interactions", () => {
|
|||||||
const messageBubble = screen.getByTestId("chat-message-msg-001");
|
const messageBubble = screen.getByTestId("chat-message-msg-001");
|
||||||
expect(messageBubble.querySelector(".chat-message-avatar")).toBeNull();
|
expect(messageBubble.querySelector(".chat-message-avatar")).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("conversation rename affordances", () => {
|
||||||
|
it("renames the clicked sidebar session without selecting it", async () => {
|
||||||
|
const selectSession = vi.fn();
|
||||||
|
const renameSession = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const sessions: ChatSessionInfo[] = [
|
||||||
|
{
|
||||||
|
id: "session-001",
|
||||||
|
agentId: "agent-001",
|
||||||
|
status: "active",
|
||||||
|
title: "Alpha Chat",
|
||||||
|
lastMessagePreview: "Alpha preview",
|
||||||
|
createdAt: "2026-04-08T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "session-002",
|
||||||
|
agentId: "agent-002",
|
||||||
|
status: "active",
|
||||||
|
title: "Beta Chat",
|
||||||
|
lastMessagePreview: "Beta preview",
|
||||||
|
createdAt: "2026-04-07T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-04-07T00:00:00.000Z",
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
setupMockChat({
|
||||||
|
sessions,
|
||||||
|
filteredSessions: sessions,
|
||||||
|
activeSession: sessions[0],
|
||||||
|
selectSession,
|
||||||
|
renameSession,
|
||||||
|
});
|
||||||
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
const betaRow = screen.getByTestId("chat-session-session-002");
|
||||||
|
await userEvent.click(within(betaRow).getByTestId("chat-session-rename-btn"));
|
||||||
|
|
||||||
|
expect(selectSession).not.toHaveBeenCalled();
|
||||||
|
const dialog = screen.getByRole("dialog", { name: /rename conversation/i });
|
||||||
|
const input = within(dialog).getByTestId("chat-rename-input") as HTMLInputElement;
|
||||||
|
expect(input).toHaveValue("Beta Chat");
|
||||||
|
|
||||||
|
await userEvent.clear(input);
|
||||||
|
await userEvent.type(input, "Renamed Beta");
|
||||||
|
await userEvent.click(within(dialog).getByTestId("chat-rename-save"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(renameSession).toHaveBeenCalledWith("session-002", "Renamed Beta");
|
||||||
|
});
|
||||||
|
expect(selectSession).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
await userEvent.click(betaRow);
|
||||||
|
expect(selectSession).toHaveBeenCalledWith("session-002");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("uses Untitled in the sidebar rename button name for empty session titles", async () => {
|
||||||
|
const renameSession = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const untitledSession: ChatSessionInfo = {
|
||||||
|
id: "session-empty-title",
|
||||||
|
agentId: "agent-001",
|
||||||
|
status: "active",
|
||||||
|
title: undefined,
|
||||||
|
createdAt: "2026-04-08T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
setupMockChat({
|
||||||
|
sessions: [untitledSession],
|
||||||
|
filteredSessions: [untitledSession],
|
||||||
|
activeSession: null,
|
||||||
|
renameSession,
|
||||||
|
});
|
||||||
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
const row = screen.getByTestId("chat-session-session-empty-title");
|
||||||
|
const renameButton = within(row).getByRole("button", { name: /rename conversation untitled/i });
|
||||||
|
expect(renameButton).toHaveAttribute("data-testid", "chat-session-rename-btn");
|
||||||
|
|
||||||
|
await userEvent.click(renameButton);
|
||||||
|
const dialog = screen.getByRole("dialog", { name: /rename conversation/i });
|
||||||
|
const input = within(dialog).getByTestId("chat-rename-input") as HTMLInputElement;
|
||||||
|
expect(input).toHaveValue("");
|
||||||
|
expect(input).toHaveAttribute("placeholder", "Untitled");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps the existing context-menu rename path wired to renameSession", async () => {
|
||||||
|
const renameSession = vi.fn().mockResolvedValue(undefined);
|
||||||
|
const session: ChatSessionInfo = {
|
||||||
|
id: "session-context",
|
||||||
|
agentId: "agent-001",
|
||||||
|
status: "active",
|
||||||
|
title: "Context Rename",
|
||||||
|
createdAt: "2026-04-08T00:00:00.000Z",
|
||||||
|
updatedAt: "2026-04-08T00:00:00.000Z",
|
||||||
|
};
|
||||||
|
|
||||||
|
setupMockChat({
|
||||||
|
sessions: [session],
|
||||||
|
filteredSessions: [session],
|
||||||
|
activeSession: session,
|
||||||
|
renameSession,
|
||||||
|
});
|
||||||
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
fireEvent.contextMenu(screen.getByTestId("chat-session-session-context"), { clientX: 12, clientY: 24 });
|
||||||
|
await userEvent.click(screen.getByTestId("chat-context-rename"));
|
||||||
|
|
||||||
|
const dialog = screen.getByRole("dialog", { name: /rename conversation/i });
|
||||||
|
const input = within(dialog).getByTestId("chat-rename-input") as HTMLInputElement;
|
||||||
|
expect(input).toHaveValue("Context Rename");
|
||||||
|
|
||||||
|
await userEvent.clear(input);
|
||||||
|
await userEvent.type(input, "Context Renamed");
|
||||||
|
await userEvent.click(within(dialog).getByTestId("chat-rename-save"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(renameSession).toHaveBeenCalledWith("session-context", "Context Renamed");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -268,6 +268,78 @@ describe("ChatView mobile behavior", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it("mobile mode: direct sidebar rows expose rename and delete buttons", async () => {
|
||||||
|
const restoreMatchMedia = mockMobileViewport();
|
||||||
|
const selectSession = vi.fn();
|
||||||
|
try {
|
||||||
|
const sessions = [
|
||||||
|
{ id: "session-001", agentId: "agent-001", status: "active" as const, title: "Mobile Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" },
|
||||||
|
];
|
||||||
|
setupMockChat({
|
||||||
|
sessions,
|
||||||
|
filteredSessions: sessions,
|
||||||
|
activeSession: null,
|
||||||
|
selectSession,
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
const row = screen.getByTestId("chat-session-session-001");
|
||||||
|
const renameButton = within(row).getByTestId("chat-session-rename-btn");
|
||||||
|
const deleteButton = within(row).getByTestId("chat-session-delete-btn");
|
||||||
|
expect(renameButton).toHaveAccessibleName(/rename conversation mobile chat/i);
|
||||||
|
expect(deleteButton).toHaveAccessibleName(/delete conversation/i);
|
||||||
|
expect(renameButton.compareDocumentPosition(deleteButton) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||||
|
expect(renameButton.closest(".chat-session-actions")).toBe(deleteButton.closest(".chat-session-actions"));
|
||||||
|
|
||||||
|
await userEvent.click(renameButton);
|
||||||
|
expect(selectSession).not.toHaveBeenCalled();
|
||||||
|
expect(screen.getByRole("dialog", { name: /rename conversation/i })).toBeInTheDocument();
|
||||||
|
} finally {
|
||||||
|
restoreMatchMedia.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it("mobile mode: quick session switcher rename affordance remains wired", async () => {
|
||||||
|
const restoreMatchMedia = mockMobileViewport();
|
||||||
|
const selectSession = vi.fn();
|
||||||
|
const renameSession = vi.fn().mockResolvedValue(undefined);
|
||||||
|
try {
|
||||||
|
const sessions = [
|
||||||
|
{ id: "session-001", agentId: "agent-001", status: "active" as const, title: "Test Chat", createdAt: "2026-04-08T00:00:00.000Z", updatedAt: "2026-04-08T00:00:00.000Z" },
|
||||||
|
{ id: "session-002", agentId: "agent-002", status: "active" as const, title: "Switcher Chat", createdAt: "2026-04-07T00:00:00.000Z", updatedAt: "2026-04-07T00:00:00.000Z" },
|
||||||
|
];
|
||||||
|
setupMockChat({
|
||||||
|
sessions,
|
||||||
|
filteredSessions: sessions,
|
||||||
|
activeSession: sessions[0],
|
||||||
|
selectSession,
|
||||||
|
renameSession,
|
||||||
|
});
|
||||||
|
|
||||||
|
await renderWithAct(<ChatView projectId="proj-123" addToast={vi.fn()} />);
|
||||||
|
|
||||||
|
await userEvent.click(screen.getByTestId("chat-mobile-session-trigger"));
|
||||||
|
await userEvent.click(screen.getByTestId("chat-mobile-session-rename-session-002"));
|
||||||
|
|
||||||
|
expect(selectSession).not.toHaveBeenCalled();
|
||||||
|
expect(screen.queryByTestId("chat-mobile-session-dropdown")).not.toBeInTheDocument();
|
||||||
|
const dialog = screen.getByRole("dialog", { name: /rename conversation/i });
|
||||||
|
const input = within(dialog).getByTestId("chat-rename-input") as HTMLInputElement;
|
||||||
|
expect(input).toHaveValue("Switcher Chat");
|
||||||
|
|
||||||
|
await userEvent.clear(input);
|
||||||
|
await userEvent.type(input, "Switcher Renamed");
|
||||||
|
await userEvent.click(within(dialog).getByTestId("chat-rename-save"));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(renameSession).toHaveBeenCalledWith("session-002", "Switcher Renamed");
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
restoreMatchMedia.mockRestore();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
it("mobile mode: thread header title opens quick session switcher and closes after selection", async () => {
|
it("mobile mode: thread header title opens quick session switcher and closes after selection", async () => {
|
||||||
const restoreMatchMedia = mockMobileViewport();
|
const restoreMatchMedia = mockMobileViewport();
|
||||||
const selectSession = vi.fn();
|
const selectSession = vi.fn();
|
||||||
|
|||||||
Reference in New Issue
Block a user