FN-7328: keep Planner Chat controls compact
Keep Planner Chat compact while preserving expanded mobile task context. - Match the Planner Chat composer sizing to Activity chat on desktop and mobile. - Keep the expanded mobile detail tab bar, priority, and execution-mode controls visible while hiding lower-priority metadata. - Move and constrain the planner model badge so header actions stay compact. - Cover the responsive layout and header ordering with dashboard tests. - Add a patch changeset for the published Fusion package. Files changed: .changeset/fn-7328-planner-chat-compact.md | 7 +++ .../dashboard/app/components/TaskDetailModal.css | 21 +++++++-- .../app/components/TaskPlannerChatTab.css | 51 +++++++++++++++++----- .../app/components/TaskPlannerChatTab.tsx | 10 ++--- ...etailModal.responsive-and-dependencies.test.tsx | 26 +++++++++-- .../__tests__/TaskPlannerChatTab.test.tsx | 2 + 6 files changed, 93 insertions(+), 24 deletions(-) Fusion-Task-Id: FN-7328 Fusion-Task-Lineage: b787d818-e65c-486b-b5f7-cd0881bf3ee0 Co-authored-by: Fusion (runfusion.ai) <noreply@runfusion.ai>
This commit is contained in:
7
.changeset/fn-7328-planner-chat-compact.md
Normal file
7
.changeset/fn-7328-planner-chat-compact.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
"@runfusion/fusion": patch
|
||||
---
|
||||
|
||||
summary: Keep Planner Chat compact while preserving expanded task controls.
|
||||
category: fix
|
||||
dev: Aligns Planner Chat composer height with Activity chat and keeps Priority, Execution Mode, and tabs visible in expanded mobile task details.
|
||||
@@ -868,7 +868,10 @@ The same expanded-mode chrome collapse now applies to all Activity segments: Liv
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailPlannerChat 2026-06-30-23:58:
|
||||
Planner Chat defaults to a focused state, but operator steering says large screens should keep the full task-detail context visible. Desktop therefore uses the same internal-scroll chat body without hiding rows; narrow/mobile layouts collapse nonessential title/meta/tabs/actions so the composer remains reachable while header identity and close/back controls stay available.
|
||||
Planner Chat defaults to a focused state, but operator steering says large screens should keep the full task-detail context visible. Desktop therefore uses the same internal-scroll chat body without hiding rows; narrow/mobile layouts collapse nonessential title/provenance/actions so the composer remains reachable while header identity and close/back controls stay available.
|
||||
|
||||
FNXC:TaskDetailPlannerChat 2026-06-30-20:06:
|
||||
Expanded Planner Chat must still expose task Priority, Execution Mode, and the tab bar on mobile. Hide only lower-priority metadata in the metadata row so inline handlers and tab navigation remain reachable without overlapping the transcript/composer.
|
||||
*/
|
||||
.detail-section--planner-chat {
|
||||
display: flex;
|
||||
@@ -1168,12 +1171,12 @@ FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset a
|
||||
|
||||
.task-detail-content--planner-chat-expanded .detail-heading-row,
|
||||
.task-detail-content--planner-chat-expanded .detail-description-toggle,
|
||||
.task-detail-content--planner-chat-expanded .detail-meta,
|
||||
.task-detail-content--planner-chat-expanded .detail-near-duplicate-banner,
|
||||
.task-detail-content--planner-chat-expanded .detail-tabs,
|
||||
.task-detail-content--planner-chat-expanded .branch-group-card,
|
||||
.task-detail-content--planner-chat-expanded .workspace-worktrees-summary,
|
||||
.task-detail-content--planner-chat-expanded .modal-actions {
|
||||
.task-detail-content--planner-chat-expanded .modal-actions,
|
||||
.task-detail-content--planner-chat-expanded .detail-provenance,
|
||||
.task-detail-content--planner-chat-expanded .detail-timestamps {
|
||||
display: none;
|
||||
}
|
||||
|
||||
@@ -1183,6 +1186,16 @@ FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset a
|
||||
padding-block: var(--space-sm);
|
||||
}
|
||||
|
||||
.task-detail-content--planner-chat-expanded .detail-meta {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.task-detail-content--planner-chat-expanded .detail-tabs {
|
||||
flex: 0 0 auto;
|
||||
margin-bottom: var(--space-sm);
|
||||
}
|
||||
|
||||
.task-detail-content--planner-chat-expanded .detail-body--planner-chat {
|
||||
padding: var(--space-sm);
|
||||
}
|
||||
|
||||
@@ -13,12 +13,17 @@
|
||||
gap: var(--space-md);
|
||||
}
|
||||
|
||||
.task-planner-chat-header > :first-child {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-planner-chat-header-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: flex-end;
|
||||
gap: var(--space-sm);
|
||||
flex-wrap: wrap;
|
||||
flex: 0 1 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.task-planner-chat-expand-toggle {
|
||||
@@ -39,7 +44,12 @@
|
||||
}
|
||||
|
||||
.task-planner-chat-model {
|
||||
flex: 0 0 auto;
|
||||
flex: 1 1 auto;
|
||||
min-width: 0;
|
||||
max-inline-size: min(100%, calc(var(--space-2xl) * 8));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
border: var(--btn-border-width) solid var(--border);
|
||||
border-radius: var(--radius-pill);
|
||||
padding: var(--space-xs) var(--space-sm);
|
||||
@@ -203,10 +213,13 @@
|
||||
|
||||
/*
|
||||
FNXC:TaskDetailPlannerChat 2026-06-30-00:00:
|
||||
The Planner Chat composer should behave like a compact input/button row on desktop: match the textarea's default height to the Send button and let flex wrapping, not a forced column, decide when the controls no longer fit inline.
|
||||
The Planner Chat composer should behave like a compact input/button row on desktop: match the textarea's default height to the Activity chat input and let flex wrapping, not a forced column, decide when the controls no longer fit inline.
|
||||
|
||||
FNXC:TaskDetailPlannerChat 2026-06-30-23:58:
|
||||
Keep the composer outside the transcript scroller and make the panel fill the modal body. Loading, errors, starters, history, and streaming content scroll inside `.task-planner-chat-transcript`, while the input row remains pinned and reachable in normal and mobile-expanded detail layouts.
|
||||
|
||||
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.
|
||||
*/
|
||||
.task-planner-chat-composer {
|
||||
display: flex;
|
||||
@@ -220,14 +233,16 @@ Keep the composer outside the transcript scroller and make the panel fill the mo
|
||||
box-sizing: border-box;
|
||||
flex: 1 1 calc(var(--space-2xl) * 8);
|
||||
min-width: min(100%, calc(var(--space-2xl) * 8));
|
||||
height: calc(var(--space-lg) + (var(--space-sm) * 2) + (var(--btn-border-width) * 2));
|
||||
min-height: calc(var(--space-lg) + (var(--space-sm) * 2) + (var(--btn-border-width) * 2));
|
||||
height: calc(var(--space-2xl) + var(--space-sm));
|
||||
min-height: calc(var(--space-2xl) + var(--space-sm));
|
||||
max-height: var(--task-chat-composer-max-height, 40vh);
|
||||
padding: var(--btn-padding);
|
||||
resize: vertical;
|
||||
}
|
||||
|
||||
.task-planner-chat-send {
|
||||
flex: 0 0 auto;
|
||||
min-block-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
@@ -235,20 +250,33 @@ Keep the composer outside the transcript scroller and make the panel fill the mo
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.task-planner-chat-header,
|
||||
.task-planner-chat-header {
|
||||
align-items: flex-start;
|
||||
gap: var(--space-sm);
|
||||
}
|
||||
|
||||
.task-planner-chat-header-actions {
|
||||
flex: 0 1 auto;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.task-planner-chat-model {
|
||||
max-inline-size: calc(var(--space-2xl) * 5);
|
||||
}
|
||||
|
||||
.task-planner-chat-expand-toggle {
|
||||
min-inline-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
min-block-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
}
|
||||
|
||||
.task-planner-chat-composer {
|
||||
flex-direction: column;
|
||||
align-items: stretch;
|
||||
}
|
||||
|
||||
.task-planner-chat-header-actions {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.task-planner-chat-expand-toggle {
|
||||
align-self: flex-end;
|
||||
min-inline-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
min-block-size: calc(var(--space-2xl) + var(--space-sm));
|
||||
.task-planner-chat-input {
|
||||
height: calc(var(--space-2xl) + var(--space-lg));
|
||||
min-height: calc(var(--space-2xl) + var(--space-lg));
|
||||
}
|
||||
|
||||
.task-planner-chat-starters {
|
||||
@@ -258,6 +286,7 @@ Keep the composer outside the transcript scroller and make the panel fill the mo
|
||||
.task-planner-chat-send {
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
min-block-size: calc(var(--space-2xl) + var(--space-lg));
|
||||
}
|
||||
|
||||
.task-planner-chat-message {
|
||||
|
||||
@@ -495,6 +495,11 @@ export function TaskPlannerChatTab({ task, projectId, active, expanded = false,
|
||||
<p>{t("taskDetail.plannerChat.description", "Ask planning questions about this task's current status, recent activity, blockers, next steps, or definition.")}</p>
|
||||
</div>
|
||||
<div className="task-planner-chat-header-actions">
|
||||
{isUsableModel(planningModel) && (
|
||||
<span className="task-planner-chat-model" data-testid="task-planner-chat-model">
|
||||
{planningModel.provider}/{planningModel.modelId}
|
||||
</span>
|
||||
)}
|
||||
{onExpandedChange && (
|
||||
<button
|
||||
type="button"
|
||||
@@ -508,11 +513,6 @@ export function TaskPlannerChatTab({ task, projectId, active, expanded = false,
|
||||
{expanded ? <Minimize2 aria-hidden="true" /> : <Maximize2 aria-hidden="true" />}
|
||||
</button>
|
||||
)}
|
||||
{isUsableModel(planningModel) && (
|
||||
<span className="task-planner-chat-model" data-testid="task-planner-chat-model">
|
||||
{planningModel.provider}/{planningModel.modelId}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -92,28 +92,39 @@ describe("TaskDetailModal", () => {
|
||||
describe("mobile responsive structure", () => {
|
||||
it("keeps planner chat composer usable on narrow task-detail layouts", () => {
|
||||
const css = readDashboardStylesSource();
|
||||
const headerBlock = getExactCssRuleBlock(css, ".task-planner-chat-header");
|
||||
const headerActionsBlock = getExactCssRuleBlock(css, ".task-planner-chat-header-actions");
|
||||
const modelBlock = getExactCssRuleBlock(css, ".task-planner-chat-model");
|
||||
const composerBlock = getExactCssRuleBlock(css, ".task-planner-chat-composer");
|
||||
const inputBlock = getExactCssRuleBlock(css, ".task-planner-chat-input");
|
||||
const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".task-planner-chat-composer");
|
||||
const mobileComposerBlock = getCssRuleBlock(mobileBlock, ".task-planner-chat-composer");
|
||||
const mobileInputBlock = getCssRuleBlock(mobileBlock, ".task-planner-chat-input");
|
||||
const mobileSendBlock = getCssRuleBlock(mobileBlock, ".task-planner-chat-send");
|
||||
|
||||
expectBaseRule(css, ".task-planner-chat", "display: flex;");
|
||||
expectBaseRule(css, ".task-planner-chat", "min-height: 0;");
|
||||
expectBaseRule(css, ".task-planner-chat-transcript", "overflow: auto;");
|
||||
expectBaseRule(css, ".task-planner-chat-transcript", "min-height: 0;");
|
||||
expect(headerBlock).toContain("justify-content: space-between;");
|
||||
expect(headerActionsBlock).toContain("justify-content: flex-end;");
|
||||
expect(headerActionsBlock).not.toContain("flex-wrap: wrap;");
|
||||
expect(modelBlock).toContain("text-overflow: ellipsis;");
|
||||
expect(composerBlock).toContain("display: flex;");
|
||||
expect(composerBlock).toContain("flex-wrap: wrap;");
|
||||
expect(composerBlock).toContain("align-items: stretch;");
|
||||
expect(composerBlock).toContain("flex: 0 0 auto;");
|
||||
expect(composerBlock).not.toContain("flex-direction: column;");
|
||||
expect(inputBlock).toContain("height: calc(var(--space-lg) + (var(--space-sm) * 2) + (var(--btn-border-width) * 2));");
|
||||
expect(inputBlock).toContain("min-height: calc(var(--space-lg) + (var(--space-sm) * 2) + (var(--btn-border-width) * 2));");
|
||||
expect(inputBlock).toContain("height: calc(var(--space-2xl) + var(--space-sm));");
|
||||
expect(inputBlock).toContain("min-height: calc(var(--space-2xl) + var(--space-sm));");
|
||||
expect(inputBlock).not.toContain("min-height: 5rem;");
|
||||
expect(mobileComposerBlock).toContain("flex-direction: column;");
|
||||
expect(mobileComposerBlock).toContain("align-items: stretch;");
|
||||
expect(mobileInputBlock).toContain("height: calc(var(--space-2xl) + var(--space-lg));");
|
||||
expect(mobileInputBlock).toContain("min-height: calc(var(--space-2xl) + var(--space-lg));");
|
||||
expect(mobileSendBlock).toContain("justify-content: center;");
|
||||
expect(mobileSendBlock).toContain("width: 100%;");
|
||||
expect(mobileSendBlock).toContain("min-block-size: calc(var(--space-2xl) + var(--space-lg));");
|
||||
expectBaseRule(css, ".task-planner-chat-starters", "grid-template-columns: repeat(2, minmax(0, 1fr));");
|
||||
expectBaseRule(css, ".task-planner-chat-message .chat-question-response", "overflow-wrap: anywhere;");
|
||||
expect(mobileBlock).toContain(".task-planner-chat-starters");
|
||||
@@ -122,13 +133,20 @@ describe("TaskDetailModal", () => {
|
||||
expect(mobileBlock).toContain("margin-inline: 0;");
|
||||
|
||||
const detailCss = readDashboardStylesSource();
|
||||
const plannerMobileBlock = getCssAtRuleBlockContaining(detailCss, "@media (max-width: 768px)", ".task-detail-content--planner-chat-expanded .detail-tabs");
|
||||
const plannerMobileBlock = getCssAtRuleBlockContaining(detailCss, "@media (max-width: 768px)", ".task-detail-content--planner-chat-expanded .detail-meta");
|
||||
const plannerExpandedTabsBlock = getCssRuleBlock(plannerMobileBlock, ".task-detail-content--planner-chat-expanded .detail-tabs");
|
||||
const plannerExpandedMetaBlock = getCssRuleBlock(plannerMobileBlock, ".task-detail-content--planner-chat-expanded .detail-meta");
|
||||
expectBaseRule(detailCss, ".detail-body--planner-chat", "overflow-y: hidden;");
|
||||
expectBaseRule(detailCss, ".detail-section--planner-chat", "min-height: 0;");
|
||||
expect(plannerMobileBlock).toContain(".task-detail-content--planner-chat-expanded .detail-heading-row");
|
||||
expect(plannerMobileBlock).toContain(".task-detail-content--planner-chat-expanded .detail-tabs");
|
||||
expect(plannerMobileBlock).toContain(".task-detail-content--planner-chat-expanded .detail-provenance");
|
||||
expect(plannerMobileBlock).toContain(".task-detail-content--planner-chat-expanded .detail-timestamps");
|
||||
expect(plannerMobileBlock).toContain(".task-detail-content--planner-chat-expanded .modal-actions");
|
||||
expect(plannerMobileBlock).toContain("display: none;");
|
||||
expect(plannerMobileBlock).not.toMatch(/\.task-detail-content--planner-chat-expanded \.detail-meta,\s*\.task-detail-content--planner-chat-expanded \.detail-near-duplicate-banner/);
|
||||
expect(plannerMobileBlock).not.toMatch(/\.task-detail-content--planner-chat-expanded \.detail-tabs,\s*\.task-detail-content--planner-chat-expanded \.branch-group-card/);
|
||||
expect(plannerExpandedMetaBlock).toContain("flex: 0 0 auto;");
|
||||
expect(plannerExpandedTabsBlock).toContain("flex: 0 0 auto;");
|
||||
});
|
||||
|
||||
it("keeps detail metadata as a single wrapping flex row without mobile column fallbacks", () => {
|
||||
|
||||
@@ -129,8 +129,10 @@ describe("TaskPlannerChatTab", () => {
|
||||
|
||||
expect(await screen.findByTestId("task-planner-chat-empty")).toBeInTheDocument();
|
||||
const toggle = screen.getByTestId("task-planner-chat-expand-toggle");
|
||||
const modelBadge = screen.getByTestId("task-planner-chat-model");
|
||||
expect(toggle).toHaveAccessibleName("Collapse planner chat");
|
||||
expect(toggle).toHaveAttribute("aria-expanded", "true");
|
||||
expect(modelBadge.compareDocumentPosition(toggle) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
||||
expect(screen.getByTestId("task-planner-chat-panel")).toContainElement(screen.getByLabelText("Message planner chat"));
|
||||
expect(screen.getByTestId("task-planner-chat-panel")).toContainElement(screen.getByRole("button", { name: "Send" }));
|
||||
|
||||
|
||||
Reference in New Issue
Block a user