FN-7634: widen Planner Chat stop button to match send button

Fix the Planner Chat composer's streaming Stop button rendering narrower than the idle Send button by giving both variants a shared width floor.

- Declare a locally-scoped --chat-input-control-size on .task-planner-chat-composer (same formula as ChatView.css's .chat-input-row so the shared .chat-input-send/.chat-input-stop classes no longer read an undefined custom property and fall back to width: auto.
- Add a min-inline-size floor bound to that property on .task-planner-chat-send (present on both send and stop variants) so neither button renders narrower than the other on desktop.
- Add a regression test asserting the desktop control-size floor, the pre-existing mobile square sizing, and the FN-7594 stop-icon visibility contract.
- Add a changeset documenting the fix as a patch release.

Files changed:
 .changeset/fn-7634-planner-stop-button-width.md    |  7 +++++
 .../app/components/TaskPlannerChatTab.css          |  8 ++++++
 .../__tests__/TaskPlannerChatTab.test.tsx          | 33 ++++++++++++++++++++++
 3 files changed, 48 insertions(+)
EOF
)

Fusion-Task-Id: FN-7634

Fusion-Task-Lineage: e73c836f-3c2d-4871-ac93-fdf5e52de5f6

Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-07 08:45:53 -07:00
parent 413ef1dfa2
commit 51e389199a
3 changed files with 48 additions and 0 deletions

View File

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

View File

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

View File

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