From a4ef439212c51507e6b5d70a6f6d538a3d75ff10 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Wed, 1 Jul 2026 10:30:45 -0700 Subject: [PATCH] FN-7376: preserve planner chat stop icon Keep Planner Chat mobile action text hiding from clipping the streaming stop icon. - Narrow the mobile Planner Chat send-button label selector to leave the shared stop icon span visible. - Add regression coverage for send and stop icon visibility during planner thinking. - Add a patch changeset for the published Fusion package. Files changed: .changeset/fn-7376-planner-chat-stop-icon.md | 7 ++++ .../app/components/TaskPlannerChatTab.css | 6 +++- .../__tests__/TaskPlannerChatTab.test.tsx | 41 ++++++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) Fusion-Task-Id: FN-7376 Fusion-Task-Lineage: 45ef720e-5ae1-4890-a2ed-afc4f97a8bb0 Co-authored-by: Fusion (runfusion.ai) --- .changeset/fn-7376-planner-chat-stop-icon.md | 7 ++++ .../app/components/TaskPlannerChatTab.css | 6 ++- .../__tests__/TaskPlannerChatTab.test.tsx | 41 +++++++++++++++++++ 3 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 .changeset/fn-7376-planner-chat-stop-icon.md diff --git a/.changeset/fn-7376-planner-chat-stop-icon.md b/.changeset/fn-7376-planner-chat-stop-icon.md new file mode 100644 index 0000000000..4c3fc51006 --- /dev/null +++ b/.changeset/fn-7376-planner-chat-stop-icon.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Keep the Planner Chat stop-generation icon visible on mobile while thinking. +category: fix +dev: Narrows the mobile Planner Chat text-hiding selector so the shared chat stop icon span remains visible. diff --git a/packages/dashboard/app/components/TaskPlannerChatTab.css b/packages/dashboard/app/components/TaskPlannerChatTab.css index 76941e2c23..650b8e5806 100644 --- a/packages/dashboard/app/components/TaskPlannerChatTab.css +++ b/packages/dashboard/app/components/TaskPlannerChatTab.css @@ -263,7 +263,11 @@ Mobile Planner Chat should match regular task chat: keep the composer as a singl padding: 0; } - .task-planner-chat-send span { + /* + FNXC:TaskDetailPlannerChat 2026-07-01-08:09: + Mobile Planner Chat hides compact action text labels, but the shared thinking/streaming stop affordance draws its icon with `.chat-input-stop-icon` on a span. Exclude that icon span so the stop button never becomes an empty-looking shell while preserving the first-tap send/stop button contract. + */ + .task-planner-chat-send span:not(.chat-input-stop-icon) { position: absolute; width: 1px; height: 1px; diff --git a/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx index 57d4e71b54..1d5bf3af97 100644 --- a/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx @@ -1,9 +1,13 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; import { describe, it, expect, vi, beforeEach } from "vitest"; import React from "react"; import { act, fireEvent, render, screen, waitFor } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { TaskPlannerChatTab } from "../TaskPlannerChatTab"; +const taskPlannerChatCss = readFileSync(resolve(__dirname, "../TaskPlannerChatTab.css"), "utf8"); + const { mockEnsureTaskPlannerChatSession, mockFetchTaskPlannerChatSession, mockFetchChatSession, mockFetchChatMessages, mockFetchTaskDetail, mockStreamChatResponse, mockAttachChatStream, mockTranslations, mockT } = vi.hoisted(() => { const translations = new Map(); return { @@ -622,6 +626,43 @@ describe("TaskPlannerChatTab", () => { ); }); + it("keeps send and thinking stop icons visible in planner chat", async () => { + const user = userEvent.setup(); + let streamHandlers: any; + mockStreamChatResponse.mockImplementation((_sessionId, _content, handlers) => { + streamHandlers = handlers; + return { close: vi.fn(), isConnected: () => true }; + }); + renderPlannerChat(); + await screen.findByTestId("task-planner-chat-empty"); + + await user.type(screen.getByLabelText("Message planner chat"), "Think with an icon"); + const sendButton = screen.getByTestId("chat-send-btn"); + expect(sendButton).toHaveAccessibleName("Send"); + expect(sendButton.querySelector("svg")).toBeTruthy(); + expect(sendButton.querySelector("span")).toHaveTextContent("Send"); + + fireEvent.pointerDown(sendButton, { pointerType: "touch" }); + act(() => { + streamHandlers.onThinking?.("checking the plan"); + }); + + const stopButton = await screen.findByTestId("chat-stop-btn"); + expect(stopButton).toHaveAccessibleName("Stop generation"); + const stopIcon = stopButton.querySelector(".chat-input-stop-icon"); + expect(stopIcon).toBeTruthy(); + expect(stopIcon).toHaveAttribute("aria-hidden", "true"); + expect(stopButton).toHaveTextContent("Stop generation"); + expect(screen.getByText("Thinking…")).toBeInTheDocument(); + expect(screen.getByText("checking the plan")).toBeInTheDocument(); + + const mobileTextHideRule = taskPlannerChatCss.match(/@media \(max-width: 768px\)[\s\S]*?\.task-planner-chat-send[^{}]*\{[^}]*clip:[^}]*\}/)?.[0] ?? ""; + expect(mobileTextHideRule).toContain("clip:"); + expect(mobileTextHideRule).toMatch(/span:not\(\.chat-input-stop-icon\)/); + expect(mobileTextHideRule).not.toMatch(/\.task-planner-chat-send\s+span\s*\{/); + expect(stopIcon).toHaveClass("chat-input-stop-icon"); + }); + it("renders live and stored thinking output through the standard chat surface", async () => { const user = userEvent.setup(); mockFetchChatMessages.mockResolvedValueOnce({