diff --git a/.changeset/fn-7634-planner-stop-button-width.md b/.changeset/fn-7634-planner-stop-button-width.md new file mode 100644 index 0000000000..2b55af6270 --- /dev/null +++ b/.changeset/fn-7634-planner-stop-button-width.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Fix the task-detail Planner Chat stop button rendering narrower than its send button. +category: fix +dev: `TaskPlannerChatTab.css` now declares a locally-scoped `--chat-input-control-size` on `.task-planner-chat-composer` (same formula as `ChatView.css`'s `.chat-input-row`) and applies it as a `min-inline-size` floor on `.task-planner-chat-send`, so the shared `.chat-input-send`/`.chat-input-stop` classes (which previously read an undefined custom property inside the Planner composer and fell back to `width: auto`) never render the streaming Stop button narrower than the idle Send button on desktop or mobile. diff --git a/packages/dashboard/app/components/TaskPlannerChatTab.css b/packages/dashboard/app/components/TaskPlannerChatTab.css index 650b8e5806..eab4987517 100644 --- a/packages/dashboard/app/components/TaskPlannerChatTab.css +++ b/packages/dashboard/app/components/TaskPlannerChatTab.css @@ -192,8 +192,14 @@ Planner Chat must keep visual height parity with Activity chat: desktop textarea FNXC:TaskDetailPlannerChat 2026-07-01-23:54: Mobile Planner Chat should match regular task chat: keep the composer as a single input row with a one-line textarea and a square send affordance on the right instead of stacking the labeled button below the input. + +FNXC:TaskDetailPlannerChat 2026-07-07-00:00: +The Planner Chat streaming Stop button must occupy the same width footprint as the Send button it replaces (no shift/shrink on swap) and mirror the regular Chat view's stop-button sizing (`.chat-input-row`'s `--chat-input-control-size` in ChatView.css). The shared `.chat-input-send` / `.chat-input-stop` classes read that custom property from `.chat-input-row`, which the Planner composer never renders inside of, so the property was undefined here and `width` fell back to `auto`, sizing each button from its own content only. Declare the same control-size formula scoped to `.task-planner-chat-composer` and give `.task-planner-chat-send` (present on both the send and stop button variants) a matching `min-inline-size` floor so neither button can render narrower than the other on desktop, without touching ChatView.css's own token. */ .task-planner-chat-composer { + /* FN-7634: same formula as ChatView.css's `.chat-input-row { --chat-input-control-size: … }` so the Planner stop/send buttons share the regular Chat view's control-size floor. */ + --chat-input-control-size: calc(var(--space-lg) * 2.5); + display: flex; flex: 0 0 auto; flex-wrap: wrap; @@ -215,6 +221,8 @@ Mobile Planner Chat should match regular task chat: keep the composer as a singl .task-planner-chat-send { flex: 0 0 auto; min-block-size: calc(var(--space-2xl) + var(--space-sm)); + /* FN-7634: floor both the Send and Stop button variants (both carry this class) at the same width so the streaming swap never narrows or widens the control. */ + min-inline-size: var(--chat-input-control-size); } @media (max-width: 768px) { diff --git a/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx index fcf8f17f33..e73cad70d7 100644 --- a/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx @@ -714,6 +714,39 @@ describe("TaskPlannerChatTab", () => { expect(stopIcon).toHaveClass("chat-input-stop-icon"); }); + it("keeps the planner stop button width in parity with the send button on desktop and mobile (FN-7634)", () => { + // Desktop: both `.chat-input-send` and `.chat-input-stop` (from ChatView.css) read + // `width: var(--chat-input-control-size)`, which is only declared inside `.chat-input-row`. + // The Planner composer must declare that same custom property itself so the value never + // resolves to `auto`, and must give the shared `.task-planner-chat-send` class (present on + // both the send and stop button variants) a `min-inline-size` floor bound to it — otherwise + // the streaming Stop button can render narrower than the idle Send button. + expect(taskPlannerChatCss).toMatch(/\.task-planner-chat-composer\s*\{[\s\S]*?--chat-input-control-size:\s*calc\(var\(--space-lg\)\s*\*\s*2\.5\);/); + + // The base (non-mobile) `.task-planner-chat-send` rule is the first one in source order, + // before the `@media (max-width: 768px)` block redeclares it for mobile. + const mediaQueryStart = taskPlannerChatCss.indexOf("@media (max-width: 768px)"); + const desktopCss = mediaQueryStart >= 0 ? taskPlannerChatCss.slice(0, mediaQueryStart) : taskPlannerChatCss; + const desktopSendRule = desktopCss.match(/\.task-planner-chat-send\s*\{[^}]*\}/)?.[0] ?? ""; + // The desktop rule must bind the same control-size custom property as its floor so the + // Send button (idle) and Stop button (streaming) — both of which carry this class — never + // differ in width footprint. + expect(desktopSendRule).toMatch(/min-inline-size:\s*var\(--chat-input-control-size\)/); + + // Mobile keeps its pre-existing explicit square sizing, which already applies identically + // to both the send and stop variants via the shared `.task-planner-chat-send` class. + const mobileSendRule = taskPlannerChatCss + .slice(mediaQueryStart) + .match(/\.task-planner-chat-send\s*\{[^}]*\}/)?.[0] ?? ""; + expect(mobileSendRule).toMatch(/inline-size:\s*calc\(var\(--space-2xl\)\s*\+\s*var\(--space-lg\)\)/); + expect(mobileSendRule).toMatch(/min-inline-size:\s*calc\(var\(--space-2xl\)\s*\+\s*var\(--space-lg\)\)/); + + // Guard the FN-7594-era contract that the stop icon span stays visible on mobile even + // though other text-label spans are visually hidden. + const mobileTextHideRule = taskPlannerChatCss.match(/@media \(max-width: 768px\)[\s\S]*?\.task-planner-chat-send[^{}]*\{[^}]*clip:[^}]*\}/)?.[0] ?? ""; + expect(mobileTextHideRule).toMatch(/span:not\(\.chat-input-stop-icon\)/); + }); + it("renders live and stored thinking output through the standard chat surface", async () => { const user = userEvent.setup(); mockFetchChatMessages.mockResolvedValueOnce({