fix(FN-7344): compact planner chat on mobile

This commit is contained in:
gsxdsm
2026-06-30 23:37:40 -07:00
parent 4a1a04303f
commit 6005f89979
4 changed files with 67 additions and 16 deletions

View File

@@ -0,0 +1,7 @@
---
"@runfusion/fusion": patch
---
summary: Keep Planner Chat compact on mobile with an inline composer and provider-icon model badge.
category: fix
dev: Adjusts TaskPlannerChatTab mobile CSS and replaces the text model badge with a ProviderIcon tooltip.

View File

@@ -44,18 +44,23 @@
} }
.task-planner-chat-model { .task-planner-chat-model {
flex: 1 1 auto; display: inline-flex;
min-width: 0; flex: 0 0 auto;
max-inline-size: min(100%, calc(var(--space-2xl) * 8)); align-items: center;
overflow: hidden; justify-content: center;
text-overflow: ellipsis; inline-size: calc(var(--space-2xl) + var(--space-sm));
white-space: nowrap; block-size: calc(var(--space-2xl) + var(--space-sm));
border: var(--btn-border-width) solid var(--border); border: var(--btn-border-width) solid var(--border);
border-radius: var(--radius-pill); border-radius: var(--radius);
padding: var(--space-xs) var(--space-sm); padding: 0;
color: var(--text-muted); color: var(--text-muted);
background: var(--surface-subtle); background: var(--surface-subtle);
font-size: var(--font-size-xs); }
.task-planner-chat-model .provider-icon {
display: inline-flex;
align-items: center;
justify-content: center;
} }
.task-planner-chat-error { .task-planner-chat-error {
@@ -220,6 +225,9 @@ Keep the composer outside the transcript scroller and make the panel fill the mo
FNXC:TaskDetailPlannerChat 2026-06-30-20:06: FNXC:TaskDetailPlannerChat 2026-06-30-20:06:
Planner Chat must keep visual height parity with Activity chat: desktop textareas use the Activity compact min-height token, and the stacked mobile composer switches to the Activity mobile touch-height token without hardcoded dimensions. Planner Chat must keep visual height parity with Activity chat: desktop textareas use the Activity compact min-height token, and the stacked mobile composer switches to the Activity mobile touch-height token without hardcoded dimensions.
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.
*/ */
.task-planner-chat-composer { .task-planner-chat-composer {
display: flex; display: flex;
@@ -261,7 +269,8 @@ Planner Chat must keep visual height parity with Activity chat: desktop textarea
} }
.task-planner-chat-model { .task-planner-chat-model {
max-inline-size: calc(var(--space-2xl) * 5); inline-size: calc(var(--space-2xl) + var(--space-sm));
block-size: calc(var(--space-2xl) + var(--space-sm));
} }
.task-planner-chat-expand-toggle { .task-planner-chat-expand-toggle {
@@ -270,13 +279,19 @@ Planner Chat must keep visual height parity with Activity chat: desktop textarea
} }
.task-planner-chat-composer { .task-planner-chat-composer {
flex-direction: column; flex-direction: row;
align-items: stretch; flex-wrap: nowrap;
align-items: flex-end;
gap: var(--space-xs);
} }
.task-planner-chat-input { .task-planner-chat-input {
flex: 1 1 auto;
min-width: 0;
height: calc(var(--space-2xl) + var(--space-lg)); height: calc(var(--space-2xl) + var(--space-lg));
min-height: calc(var(--space-2xl) + var(--space-lg)); min-height: calc(var(--space-2xl) + var(--space-lg));
max-height: calc(var(--space-2xl) + var(--space-lg));
resize: none;
} }
.task-planner-chat-starters { .task-planner-chat-starters {
@@ -284,9 +299,26 @@ Planner Chat must keep visual height parity with Activity chat: desktop textarea
} }
.task-planner-chat-send { .task-planner-chat-send {
--btn-icon-size: calc(var(--space-2xl) + var(--space-sm));
flex: 0 0 auto;
justify-content: center; justify-content: center;
width: 100%; inline-size: calc(var(--space-2xl) + var(--space-lg));
min-inline-size: calc(var(--space-2xl) + var(--space-lg));
block-size: calc(var(--space-2xl) + var(--space-lg));
min-block-size: calc(var(--space-2xl) + var(--space-lg)); min-block-size: calc(var(--space-2xl) + var(--space-lg));
padding: 0;
}
.task-planner-chat-send span {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0 0 0 0);
white-space: nowrap;
border: 0;
} }
.task-planner-chat-message { .task-planner-chat-message {

View File

@@ -11,6 +11,7 @@ import { ensureTaskPlannerChatSession, fetchChatMessages, fetchTaskDetail, fetch
import { parseQuestionToolCall, type ParsedQuestionToolCall } from "../utils/parseQuestionToolCall"; import { parseQuestionToolCall, type ParsedQuestionToolCall } from "../utils/parseQuestionToolCall";
import { markdownComponents } from "./AgentLogViewer"; import { markdownComponents } from "./AgentLogViewer";
import { ChatQuestionResponse } from "./ChatQuestionResponse"; import { ChatQuestionResponse } from "./ChatQuestionResponse";
import { ProviderIcon } from "./ProviderIcon";
import "./TaskPlannerChatTab.css"; import "./TaskPlannerChatTab.css";
interface TaskPlannerChatTabProps { interface TaskPlannerChatTabProps {
@@ -246,6 +247,7 @@ export function TaskPlannerChatTab({ task, projectId, active, expanded = false,
const planningModelProvider = isUsableModel(planningModel) ? planningModel.provider : undefined; const planningModelProvider = isUsableModel(planningModel) ? planningModel.provider : undefined;
const planningModelId = isUsableModel(planningModel) ? planningModel.modelId : undefined; const planningModelId = isUsableModel(planningModel) ? planningModel.modelId : undefined;
const planningModelLabel = planningModelProvider && planningModelId ? `${planningModelProvider}/${planningModelId}` : "";
const modelPayload = useMemo(() => { const modelPayload = useMemo(() => {
return planningModelProvider && planningModelId return planningModelProvider && planningModelId
? { modelProvider: planningModelProvider, modelId: planningModelId } ? { modelProvider: planningModelProvider, modelId: planningModelId }
@@ -505,8 +507,13 @@ export function TaskPlannerChatTab({ task, projectId, active, expanded = false,
</div> </div>
<div className="task-planner-chat-header-actions"> <div className="task-planner-chat-header-actions">
{isUsableModel(planningModel) && ( {isUsableModel(planningModel) && (
<span className="task-planner-chat-model" data-testid="task-planner-chat-model"> <span
{planningModel.provider}/{planningModel.modelId} className="task-planner-chat-model"
data-testid="task-planner-chat-model"
title={planningModelLabel}
aria-label={planningModelLabel}
>
<ProviderIcon provider={planningModel.provider} size="md" />
</span> </span>
)} )}
{onExpandedChange && ( {onExpandedChange && (
@@ -623,6 +630,7 @@ export function TaskPlannerChatTab({ task, projectId, active, expanded = false,
onChange={(event) => setDraft(event.target.value)} onChange={(event) => setDraft(event.target.value)}
onKeyDown={handleKeyDown} onKeyDown={handleKeyDown}
disabled={composerState === "sending"} disabled={composerState === "sending"}
rows={1}
/> />
<button type="button" className="btn btn-primary task-planner-chat-send" onClick={() => void sendMessage()} disabled={!canSend}> <button type="button" className="btn btn-primary task-planner-chat-send" onClick={() => void sendMessage()} disabled={!canSend}>
{composerState === "sending" ? <Loader2 className="animate-spin" aria-hidden="true" /> : <Send aria-hidden="true" />} {composerState === "sending" ? <Loader2 className="animate-spin" aria-hidden="true" /> : <Send aria-hidden="true" />}

View File

@@ -118,7 +118,11 @@ describe("TaskPlannerChatTab", () => {
); );
expect(mockEnsureTaskPlannerChatSession).not.toHaveBeenCalled(); expect(mockEnsureTaskPlannerChatSession).not.toHaveBeenCalled();
expect(mockFetchChatMessages).toHaveBeenCalledWith("chat-planner", { order: "asc" }, undefined); expect(mockFetchChatMessages).toHaveBeenCalledWith("chat-planner", { order: "asc" }, undefined);
expect(screen.getByTestId("task-planner-chat-model")).toHaveTextContent("anthropic/claude-plan"); const modelBadge = screen.getByTestId("task-planner-chat-model");
expect(modelBadge).toHaveAccessibleName("anthropic/claude-plan");
expect(modelBadge).toHaveAttribute("title", "anthropic/claude-plan");
expect(modelBadge).toHaveTextContent("");
expect(modelBadge.querySelector(".provider-icon[data-provider='anthropic']")).toBeTruthy();
expect(screen.getByRole("button", { name: /Summarize recent activity/ })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /Summarize recent activity/ })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Explain status and blockers/ })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /Explain status and blockers/ })).toBeInTheDocument();
expect(screen.getByRole("button", { name: /Identify the next best action/ })).toBeInTheDocument(); expect(screen.getByRole("button", { name: /Identify the next best action/ })).toBeInTheDocument();