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: 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 { .detail-section--planner-chat {
display: flex; 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-heading-row,
.task-detail-content--planner-chat-expanded .detail-description-toggle, .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-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 .branch-group-card,
.task-detail-content--planner-chat-expanded .workspace-worktrees-summary, .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; 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); 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 { .task-detail-content--planner-chat-expanded .detail-body--planner-chat {
padding: var(--space-sm); padding: var(--space-sm);
} }

View File

@@ -13,12 +13,17 @@
gap: var(--space-md); gap: var(--space-md);
} }
.task-planner-chat-header > :first-child {
min-width: 0;
}
.task-planner-chat-header-actions { .task-planner-chat-header-actions {
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: flex-end; justify-content: flex-end;
gap: var(--space-sm); gap: var(--space-sm);
flex-wrap: wrap; flex: 0 1 auto;
min-width: 0;
} }
.task-planner-chat-expand-toggle { .task-planner-chat-expand-toggle {
@@ -39,7 +44,12 @@
} }
.task-planner-chat-model { .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: var(--btn-border-width) solid var(--border);
border-radius: var(--radius-pill); border-radius: var(--radius-pill);
padding: var(--space-xs) var(--space-sm); padding: var(--space-xs) var(--space-sm);
@@ -203,10 +213,13 @@
/* /*
FNXC:TaskDetailPlannerChat 2026-06-30-00:00: 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: 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. 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 { .task-planner-chat-composer {
display: flex; display: flex;
@@ -220,14 +233,16 @@ Keep the composer outside the transcript scroller and make the panel fill the mo
box-sizing: border-box; box-sizing: border-box;
flex: 1 1 calc(var(--space-2xl) * 8); flex: 1 1 calc(var(--space-2xl) * 8);
min-width: min(100%, 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)); height: calc(var(--space-2xl) + var(--space-sm));
min-height: calc(var(--space-lg) + (var(--space-sm) * 2) + (var(--btn-border-width) * 2)); min-height: calc(var(--space-2xl) + var(--space-sm));
max-height: var(--task-chat-composer-max-height, 40vh);
padding: var(--btn-padding); padding: var(--btn-padding);
resize: vertical; resize: vertical;
} }
.task-planner-chat-send { .task-planner-chat-send {
flex: 0 0 auto; flex: 0 0 auto;
min-block-size: calc(var(--space-2xl) + var(--space-sm));
} }
@media (max-width: 768px) { @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; 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 { .task-planner-chat-composer {
flex-direction: column; flex-direction: column;
align-items: stretch; align-items: stretch;
} }
.task-planner-chat-header-actions { .task-planner-chat-input {
justify-content: space-between; height: calc(var(--space-2xl) + var(--space-lg));
} min-height: calc(var(--space-2xl) + var(--space-lg));
.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-starters { .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 { .task-planner-chat-send {
justify-content: center; justify-content: center;
width: 100%; width: 100%;
min-block-size: calc(var(--space-2xl) + var(--space-lg));
} }
.task-planner-chat-message { .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> <p>{t("taskDetail.plannerChat.description", "Ask planning questions about this task's current status, recent activity, blockers, next steps, or definition.")}</p>
</div> </div>
<div className="task-planner-chat-header-actions"> <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 && ( {onExpandedChange && (
<button <button
type="button" type="button"
@@ -508,11 +513,6 @@ export function TaskPlannerChatTab({ task, projectId, active, expanded = false,
{expanded ? <Minimize2 aria-hidden="true" /> : <Maximize2 aria-hidden="true" />} {expanded ? <Minimize2 aria-hidden="true" /> : <Maximize2 aria-hidden="true" />}
</button> </button>
)} )}
{isUsableModel(planningModel) && (
<span className="task-planner-chat-model" data-testid="task-planner-chat-model">
{planningModel.provider}/{planningModel.modelId}
</span>
)}
</div> </div>
</div> </div>

View File

@@ -92,28 +92,39 @@ describe("TaskDetailModal", () => {
describe("mobile responsive structure", () => { describe("mobile responsive structure", () => {
it("keeps planner chat composer usable on narrow task-detail layouts", () => { it("keeps planner chat composer usable on narrow task-detail layouts", () => {
const css = readDashboardStylesSource(); 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 composerBlock = getExactCssRuleBlock(css, ".task-planner-chat-composer");
const inputBlock = getExactCssRuleBlock(css, ".task-planner-chat-input"); const inputBlock = getExactCssRuleBlock(css, ".task-planner-chat-input");
const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".task-planner-chat-composer"); const mobileBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".task-planner-chat-composer");
const mobileComposerBlock = getCssRuleBlock(mobileBlock, ".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"); const mobileSendBlock = getCssRuleBlock(mobileBlock, ".task-planner-chat-send");
expectBaseRule(css, ".task-planner-chat", "display: flex;"); expectBaseRule(css, ".task-planner-chat", "display: flex;");
expectBaseRule(css, ".task-planner-chat", "min-height: 0;"); expectBaseRule(css, ".task-planner-chat", "min-height: 0;");
expectBaseRule(css, ".task-planner-chat-transcript", "overflow: auto;"); expectBaseRule(css, ".task-planner-chat-transcript", "overflow: auto;");
expectBaseRule(css, ".task-planner-chat-transcript", "min-height: 0;"); 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("display: flex;");
expect(composerBlock).toContain("flex-wrap: wrap;"); expect(composerBlock).toContain("flex-wrap: wrap;");
expect(composerBlock).toContain("align-items: stretch;"); expect(composerBlock).toContain("align-items: stretch;");
expect(composerBlock).toContain("flex: 0 0 auto;"); expect(composerBlock).toContain("flex: 0 0 auto;");
expect(composerBlock).not.toContain("flex-direction: column;"); 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("height: calc(var(--space-2xl) + var(--space-sm));");
expect(inputBlock).toContain("min-height: calc(var(--space-lg) + (var(--space-sm) * 2) + (var(--btn-border-width) * 2));"); expect(inputBlock).toContain("min-height: calc(var(--space-2xl) + var(--space-sm));");
expect(inputBlock).not.toContain("min-height: 5rem;"); expect(inputBlock).not.toContain("min-height: 5rem;");
expect(mobileComposerBlock).toContain("flex-direction: column;"); expect(mobileComposerBlock).toContain("flex-direction: column;");
expect(mobileComposerBlock).toContain("align-items: stretch;"); 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("justify-content: center;");
expect(mobileSendBlock).toContain("width: 100%;"); 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-starters", "grid-template-columns: repeat(2, minmax(0, 1fr));");
expectBaseRule(css, ".task-planner-chat-message .chat-question-response", "overflow-wrap: anywhere;"); expectBaseRule(css, ".task-planner-chat-message .chat-question-response", "overflow-wrap: anywhere;");
expect(mobileBlock).toContain(".task-planner-chat-starters"); expect(mobileBlock).toContain(".task-planner-chat-starters");
@@ -122,13 +133,20 @@ describe("TaskDetailModal", () => {
expect(mobileBlock).toContain("margin-inline: 0;"); expect(mobileBlock).toContain("margin-inline: 0;");
const detailCss = readDashboardStylesSource(); 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-body--planner-chat", "overflow-y: hidden;");
expectBaseRule(detailCss, ".detail-section--planner-chat", "min-height: 0;"); 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-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(".task-detail-content--planner-chat-expanded .modal-actions");
expect(plannerMobileBlock).toContain("display: none;"); 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", () => { 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(); expect(await screen.findByTestId("task-planner-chat-empty")).toBeInTheDocument();
const toggle = screen.getByTestId("task-planner-chat-expand-toggle"); 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).toHaveAccessibleName("Collapse planner chat");
expect(toggle).toHaveAttribute("aria-expanded", "true"); 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.getByLabelText("Message planner chat"));
expect(screen.getByTestId("task-planner-chat-panel")).toContainElement(screen.getByRole("button", { name: "Send" })); expect(screen.getByTestId("task-planner-chat-panel")).toContainElement(screen.getByRole("button", { name: "Send" }));