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:
gsxdsm
2026-06-30 20:56:05 -07:00
parent 4ae0456c48
commit b52f92cb7c
6 changed files with 96 additions and 27 deletions

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

View File

@@ -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);
}

View File

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

View File

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

View File

@@ -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", () => {

View File

@@ -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" }));