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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-01 10:30:45 -07:00
parent 427ce04a6f
commit a4ef439212
3 changed files with 53 additions and 1 deletions

View File

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

View File

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

View File

@@ -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<string, string>();
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({