diff --git a/.changeset/planner-chat-expanded-takeover.md b/.changeset/planner-chat-expanded-takeover.md new file mode 100644 index 0000000000..ca61d3da34 --- /dev/null +++ b/.changeset/planner-chat-expanded-takeover.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Make Planner Chat collapsed by default with a floating expand control that takes over the detail view. +category: fix +dev: Planner Chat expansion now mirrors Activity's overlay control and hides most task-detail chrome while preserving title/workflow context. diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index f3a6991d62..5b4abdb923 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -883,6 +883,9 @@ Expanded Planner Chat must still expose task Priority, Execution Mode, and the t FNXC:TaskDetailPlannerChat 2026-07-02-00:04: Expanded Planner Chat must keep the task title and workflow badge visible on mobile. The title preserves task identity, and the workflow badge is owned by the timestamps row, so this mode hides lower-priority metadata but not `.detail-heading-row` or `.detail-timestamps`; timestamp labels inside that row can hide so the workflow badge remains compact. + +FNXC:TaskDetailPlannerChat 2026-07-02-00:17: +Planner Chat should not open expanded by default. When the user expands it, the view should behave like Activity expansion: the chat panel takes nearly the whole modal, with only task title and workflow identity left above it for context. */ .detail-section--planner-chat { display: flex; @@ -895,6 +898,7 @@ Expanded Planner Chat must keep the task title and workflow badge visible on mob .task-detail-content--planner-chat-expanded .detail-body--planner-chat { flex: 1; min-height: 0; + padding: var(--space-md); } .task-detail-content--planner-chat-expanded .detail-section--planner-chat { @@ -902,6 +906,34 @@ Expanded Planner Chat must keep the task title and workflow badge visible on mob min-height: 0; } +.task-detail-content--planner-chat-expanded .detail-description-toggle, +.task-detail-content--planner-chat-expanded .detail-near-duplicate-banner, +.task-detail-content--planner-chat-expanded .task-fields-section, +.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 .detail-tabs, +.task-detail-content--planner-chat-expanded .detail-meta-inline-controls, +.task-detail-content--planner-chat-expanded .detail-provenance { + display: none; +} + +.task-detail-content--planner-chat-expanded .modal-header { + flex: 0 0 auto; + justify-content: space-between; + 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-timestamps .detail-timestamp-item, +.task-detail-content--planner-chat-expanded .detail-timestamps .detail-timestamp-separator { + display: none; +} + .detail-spec-edit-trigger { display: flex; @@ -1179,36 +1211,6 @@ FN-6500 fixes a tablet regression from FN-5599: the task-detail overlay offset a display: none; } - .task-detail-content--planner-chat-expanded .detail-description-toggle, - .task-detail-content--planner-chat-expanded .detail-near-duplicate-banner, - .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 .detail-provenance { - display: none; - } - - .task-detail-content--planner-chat-expanded .modal-header { - flex: 0 0 auto; - justify-content: space-between; - 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-timestamps .detail-timestamp-item, - .task-detail-content--planner-chat-expanded .detail-timestamps .detail-timestamp-separator { - display: none; - } - - .task-detail-content--planner-chat-expanded .detail-tabs { - flex: 0 0 auto; - margin-bottom: var(--space-sm); - } - .detail-body--planner-chat { padding: var(--space-sm); } diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index 70b6fe9781..17ca3802b4 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -557,7 +557,7 @@ export function TaskDetailContent({ const [activeTab, setActiveTab] = useState(() => resolveDefaultTab(initialTab, task.column, taskDetailChatFirst)); const [activitySegment, setActivitySegment] = useState(() => resolveDefaultActivitySegment(initialTab)); const [activityExpanded, setActivityExpanded] = useState(false); - const [plannerChatExpanded, setPlannerChatExpanded] = useState(true); + const [plannerChatExpanded, setPlannerChatExpanded] = useState(false); // ── CLI agent session (U11) ──────────────────────────────────────────────── const [cliSession, setCliSession] = useState(null); @@ -708,7 +708,7 @@ export function TaskDetailContent({ // Reset description and planner-chat focus state when task changes useEffect(() => { setDescriptionExpanded(false); - setPlannerChatExpanded(true); + setPlannerChatExpanded(false); }, [task.column, task.id]); const [highlightStallCode, setHighlightStallCode] = useState(null); diff --git a/packages/dashboard/app/components/TaskPlannerChatTab.css b/packages/dashboard/app/components/TaskPlannerChatTab.css index c5b204831b..1fc5e50bd0 100644 --- a/packages/dashboard/app/components/TaskPlannerChatTab.css +++ b/packages/dashboard/app/components/TaskPlannerChatTab.css @@ -1,4 +1,5 @@ .task-planner-chat { + position: relative; display: flex; flex: 1 1 auto; flex-direction: column; @@ -33,6 +34,20 @@ padding: 0; } +/* +FNXC:TaskDetailPlannerChat 2026-07-02-00:16: +Planner Chat expansion should use the same in-view floating affordance as Activity Live/Feed, not a header action that shifts layout. Anchor it to the chat panel so it remains visible while the transcript scrolls. +*/ +.task-planner-chat-expand-toggle--overlay { + position: absolute; + top: var(--space-md); + right: var(--space-md); + z-index: 3; + background: var(--surface); + border-color: var(--border); + box-shadow: var(--shadow-sm); +} + .task-planner-chat-header h4 { margin: 0; color: var(--text); @@ -261,6 +276,7 @@ Mobile Planner Chat should match regular task chat: keep the composer as a singl .task-planner-chat-header { align-items: flex-start; gap: var(--space-sm); + padding-right: calc(var(--space-2xl) + var(--space-lg)); } .task-planner-chat-header-actions { @@ -278,6 +294,11 @@ Mobile Planner Chat should match regular task chat: keep the composer as a singl min-block-size: calc(var(--space-2xl) + var(--space-sm)); } + .task-planner-chat-expand-toggle--overlay { + top: var(--space-sm); + right: var(--space-sm); + } + .task-planner-chat-composer { flex-direction: row; flex-wrap: nowrap; diff --git a/packages/dashboard/app/components/TaskPlannerChatTab.tsx b/packages/dashboard/app/components/TaskPlannerChatTab.tsx index 4403899cd9..8a8e73313a 100644 --- a/packages/dashboard/app/components/TaskPlannerChatTab.tsx +++ b/packages/dashboard/app/components/TaskPlannerChatTab.tsx @@ -500,6 +500,19 @@ export function TaskPlannerChatTab({ task, projectId, active, expanded = false, */ return (
+ {onExpandedChange && ( + + )}

{t("taskDetail.plannerChat.heading", "Planner Chat")}

@@ -515,19 +528,6 @@ export function TaskPlannerChatTab({ task, projectId, active, expanded = false, )} - {onExpandedChange && ( - - )}
diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx index 9a1d38e597..e63b983027 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx @@ -477,8 +477,8 @@ describe("TaskDetailModal", () => { expect(screen.queryByRole("button", { name: "Logs" })).toBeNull(); expect(screen.getByRole("button", { name: "Chat" })).toHaveClass("detail-tab-active"); expect(screen.getByTestId("task-planner-chat-panel")).toBeTruthy(); - expect(screen.getByTestId("task-planner-chat-expand-toggle")).toHaveAttribute("aria-label", "Collapse planner chat"); - expect(container.querySelector(".task-detail-content")).toHaveClass("task-detail-content--planner-chat-expanded"); + expect(screen.getByTestId("task-planner-chat-expand-toggle")).toHaveAttribute("aria-label", "Expand planner chat"); + expect(container.querySelector(".task-detail-content")).not.toHaveClass("task-detail-content--planner-chat-expanded"); expect(container.querySelector(".activity-segmented-control")).toBeNull(); expect(container.querySelector(".activity-segment")).toBeNull(); expect(screen.queryByTestId("task-chat-expand-toggle")).toBeNull(); @@ -947,7 +947,7 @@ describe("TaskDetailModal", () => { ); const content = container.querySelector(".task-detail-content"); - expect(screen.getByTestId("task-planner-chat-expand-toggle")).toHaveAttribute("aria-label", "Collapse planner chat"); + expect(screen.getByTestId("task-planner-chat-expand-toggle")).toHaveAttribute("aria-label", "Expand planner chat"); expect(screen.queryByTestId("task-chat-expand-toggle")).toBeNull(); fireEvent.click(screen.getByRole("button", { name: "Activity" })); diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx index 573e726fb6..9ffdcc0d02 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.responsive-and-dependencies.test.tsx @@ -107,6 +107,7 @@ describe("TaskDetailModal", () => { 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 expandOverlayBlock = getExactCssRuleBlock(css, ".task-planner-chat-expand-toggle--overlay"); 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"); @@ -115,9 +116,13 @@ describe("TaskDetailModal", () => { const mobileSendBlock = getCssRuleBlock(mobileBlock, ".task-planner-chat-send"); expectBaseRule(css, ".task-planner-chat", "display: flex;"); + expectBaseRule(css, ".task-planner-chat", "position: relative;"); 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(expandOverlayBlock).toContain("position: absolute;"); + expect(expandOverlayBlock).toContain("top: var(--space-md);"); + expect(expandOverlayBlock).toContain("right: var(--space-md);"); expect(headerBlock).toContain("justify-content: space-between;"); expect(headerActionsBlock).toContain("justify-content: flex-end;"); expect(headerActionsBlock).not.toContain("flex-wrap: wrap;"); @@ -150,21 +155,19 @@ 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-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"); + const plannerExpandedMetaBlock = getExactCssRuleBlock(detailCss, ".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-provenance"); - expect(plannerMobileBlock).toContain(".task-detail-content--planner-chat-expanded .modal-actions"); - expect(plannerMobileBlock).toContain("display: none;"); - expect(plannerMobileBlock).not.toContain(".task-detail-content--planner-chat-expanded .detail-heading-row"); - expect(plannerMobileBlock).not.toMatch(/\.task-detail-content--planner-chat-expanded \.detail-timestamps\s*\{/); - expect(plannerMobileBlock).toContain(".task-detail-content--planner-chat-expanded .detail-timestamps .detail-timestamp-item"); - 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(detailCss).toContain(".task-detail-content--planner-chat-expanded .detail-provenance"); + expect(detailCss).toContain(".task-detail-content--planner-chat-expanded .modal-actions"); + expect(detailCss).toContain(".task-detail-content--planner-chat-expanded .detail-tabs"); + expect(detailCss).toContain(".task-detail-content--planner-chat-expanded .detail-meta-inline-controls"); + expect(detailCss).not.toContain(".task-detail-content--planner-chat-expanded .detail-heading-row"); + expect(detailCss).not.toMatch(/\.task-detail-content--planner-chat-expanded \.detail-timestamps\s*\{/); + expect(detailCss).toContain(".task-detail-content--planner-chat-expanded .detail-timestamps .detail-timestamp-item"); + expect(detailCss).not.toMatch(/\.task-detail-content--planner-chat-expanded \.detail-meta,\s*\.task-detail-content--planner-chat-expanded \.detail-near-duplicate-banner/); + expect(detailCss).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 Planner Chat expand spacing identical across desktop and mobile", () => { @@ -178,14 +181,14 @@ describe("TaskDetailModal", () => { const plannerComposerBlock = getExactCssRuleBlock(css, ".task-planner-chat-composer"); const expandedPlannerBodyBlock = getExactCssRuleBlock(css, ".task-detail-content--planner-chat-expanded .detail-body--planner-chat"); const expandedPlannerSectionBlock = getExactCssRuleBlock(css, ".task-detail-content--planner-chat-expanded .detail-section--planner-chat"); - const mobilePlannerBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".task-detail-content--planner-chat-expanded .detail-meta"); + const mobilePlannerBlock = getCssAtRuleBlockContaining(css, "@media (max-width: 768px)", ".detail-body--planner-chat"); const mobilePlannerBodyBlock = getStandaloneCssRuleBlock(mobilePlannerBlock, ".detail-body--planner-chat"); const mobileExpandedPlannerBodyBlock = getExactCssRuleBlock(mobilePlannerBlock, ".task-detail-content--planner-chat-expanded .detail-body--planner-chat"); expect(plannerBodyBlock).toContain("padding: var(--space-md);"); expect(expandedPlannerBodyBlock).toContain("flex: 1;"); expect(expandedPlannerBodyBlock).toContain("min-height: 0;"); - expectNoSpacingOverrides(expandedPlannerBodyBlock, "desktop expanded planner body"); + expect(expandedPlannerBodyBlock).toContain("padding: var(--space-md);"); expect(expandedPlannerSectionBlock).toContain("flex: 1;"); expect(expandedPlannerSectionBlock).toContain("min-height: 0;"); expectNoSpacingOverrides(expandedPlannerSectionBlock, "desktop expanded planner section"); @@ -196,7 +199,7 @@ describe("TaskDetailModal", () => { expect(plannerTranscriptBlock).toContain("gap: var(--space-md);"); expect(plannerComposerBlock).toContain("gap: var(--space-sm);"); expect(css).not.toMatch(/task-detail-content--planner-chat-expanded[^{]+\.(?:task-planner-chat|task-planner-chat-transcript|task-planner-chat-composer)\s*\{[^}]*(?:padding|margin|gap)\s*:/); - expect(css).not.toMatch(/task-detail-content--planner-chat-expanded[^{]+\.detail-body--planner-chat\s*\{[^}]*(?:padding|margin|gap)\s*:/); + expect(css).not.toMatch(/task-detail-content--planner-chat-expanded[^{]+\.detail-body--planner-chat\s*\{[^}]*(?:margin|gap)\s*:/); }); it("keeps detail metadata as a single wrapping flex row without mobile column fallbacks", () => { diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx index 31b24bad8a..e2ad8636a0 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.test.tsx @@ -137,25 +137,25 @@ describe("TaskDetailModal planner Chat tab", () => { expect(screen.getByTestId("task-planner-chat-panel")).toBeInTheDocument(); }); - it("defaults planner Chat to focused mode and lets the in-view control collapse it", async () => { + it("defaults planner Chat to collapsed mode and lets the in-view control expand it", async () => { const user = userEvent.setup(); const { container } = renderTask("todo"); const detail = container.querySelector(".task-detail-content"); - expect(detail).toHaveClass("task-detail-content--planner-chat-expanded"); const toggle = screen.getByTestId("task-planner-chat-expand-toggle"); - expect(toggle).toHaveAccessibleName("Collapse planner chat"); - expect(toggle).toHaveAttribute("aria-expanded", "true"); + expect(detail).not.toHaveClass("task-detail-content--planner-chat-expanded"); + expect(toggle).toHaveAccessibleName("Expand planner chat"); + expect(toggle).toHaveAttribute("aria-expanded", "false"); + expect(screen.getByRole("button", { name: "Chat" })).toBeInTheDocument(); + expect(screen.getByRole("button", { name: "Activity" })).toBeInTheDocument(); await user.click(toggle); - expect(detail).not.toHaveClass("task-detail-content--planner-chat-expanded"); - expect(screen.getByRole("button", { name: "Chat" })).toBeInTheDocument(); - expect(screen.getByRole("button", { name: "Activity" })).toBeInTheDocument(); - expect(screen.getByTestId("task-planner-chat-expand-toggle")).toHaveAccessibleName("Expand planner chat"); + expect(detail).toHaveClass("task-detail-content--planner-chat-expanded"); + expect(screen.getByTestId("task-planner-chat-expand-toggle")).toHaveAccessibleName("Collapse planner chat"); }); - it("resets planner Chat focused mode when switching tasks", async () => { + it("resets planner Chat expanded mode when switching tasks", async () => { const user = userEvent.setup(); const { container, rerender } = render( { const detail = container.querySelector(".task-detail-content"); await user.click(screen.getByTestId("task-planner-chat-expand-toggle")); - expect(detail).not.toHaveClass("task-detail-content--planner-chat-expanded"); + expect(detail).toHaveClass("task-detail-content--planner-chat-expanded"); rerender( { />, ); - expect(detail).toHaveClass("task-detail-content--planner-chat-expanded"); + expect(detail).not.toHaveClass("task-detail-content--planner-chat-expanded"); }); it("keeps Activity expansion independent from planner Chat expansion", async () => { @@ -201,11 +201,11 @@ describe("TaskDetailModal planner Chat tab", () => { const chatTab = container.querySelectorAll(".detail-tabs .detail-tab")[0]; expect(chatTab?.textContent?.trim()).toBe("Chat"); fireEvent.click(chatTab!); - expect(detail).toHaveClass("task-detail-content--planner-chat-expanded"); + expect(detail).not.toHaveClass("task-detail-content--planner-chat-expanded"); expect(detail).not.toHaveClass("task-detail-content--chat-expanded"); await user.click(screen.getByTestId("task-planner-chat-expand-toggle")); - expect(detail).not.toHaveClass("task-detail-content--planner-chat-expanded"); + expect(detail).toHaveClass("task-detail-content--planner-chat-expanded"); expect(detail).not.toHaveClass("task-detail-content--chat-expanded"); }); }); diff --git a/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx index 3c22153aab..e132588ac2 100644 --- a/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskPlannerChatTab.test.tsx @@ -182,7 +182,9 @@ describe("TaskPlannerChatTab", () => { 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(toggle).toHaveClass("task-planner-chat-expand-toggle--overlay"); + expect(screen.getByTestId("task-planner-chat-panel")).toContainElement(toggle); + expect(modelBadge).toBeInTheDocument(); 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" }));