fix(FN-7344): compact planner chat on mobile
This commit is contained in:
7
.changeset/mobile-planner-chat-inline-send.md
Normal file
7
.changeset/mobile-planner-chat-inline-send.md
Normal 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.
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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" />}
|
||||||
|
|||||||
@@ -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();
|
||||||
|
|||||||
Reference in New Issue
Block a user