diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index e040bfd360..08a7a4d22c 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -690,6 +690,7 @@ For related global/project configuration behavior, see [Settings reference](./se
Inspect task definition, logs, review feedback, comments, documents, workflow outcomes, model overrides, and task routing from a single modal.
- Editable tasks with descriptions show **Summarize as title** beside the read-mode title; it asks AI to generate a concise title from the description and saves it without opening the edit form.
+- The **Chat** tab includes an expand/collapse control that lets the transcript and composer fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed.
- The priority chip in task metadata is an inline picker: you can change priority directly without entering full edit mode.
- Execution mode has a read-mode inline lightning-bolt toggle for Fast mode on/off without opening the full edit form.
- These two metadata controls share matched sizing/alignment in read mode (including mobile wrapping) so they behave like a single polished control group.
diff --git a/packages/dashboard/app/components/TaskChatTab.css b/packages/dashboard/app/components/TaskChatTab.css
index e0626b3e2c..7c6b7f6a46 100644
--- a/packages/dashboard/app/components/TaskChatTab.css
+++ b/packages/dashboard/app/components/TaskChatTab.css
@@ -7,6 +7,19 @@
height: 100%;
}
+.task-chat-toolbar {
+ display: flex;
+ flex: 0 0 auto;
+ justify-content: flex-end;
+ gap: var(--space-sm);
+}
+
+.task-chat-expand-toggle {
+ display: inline-flex;
+ align-items: center;
+ gap: var(--space-xs);
+}
+
.task-chat-transcript {
display: flex;
flex: 1 1 auto;
@@ -325,6 +338,15 @@
gap: var(--space-sm);
}
+ .task-chat-toolbar {
+ justify-content: stretch;
+ }
+
+ .task-chat-expand-toggle {
+ justify-content: center;
+ width: 100%;
+ }
+
.task-chat-transcript {
flex: 1 1 auto;
min-height: 0;
diff --git a/packages/dashboard/app/components/TaskChatTab.tsx b/packages/dashboard/app/components/TaskChatTab.tsx
index 8fda0ba96d..77e1e5e3fa 100644
--- a/packages/dashboard/app/components/TaskChatTab.tsx
+++ b/packages/dashboard/app/components/TaskChatTab.tsx
@@ -2,7 +2,7 @@ import type { AgentLogEntry, AgentRole, SteeringComment, Task, TaskDetail } from
import React, { useCallback, useLayoutEffect, useMemo, useRef, useState } from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
-import { ChevronDown, Loader2, Send } from "lucide-react";
+import { ChevronDown, Loader2, Maximize2, Minimize2, Send } from "lucide-react";
import { addSteeringComment } from "../api";
import { useAgentLogs } from "../hooks/useAgentLogs";
import type { ToastType } from "../hooks/useToast";
@@ -20,6 +20,8 @@ interface TaskChatTabProps {
addToast: (msg: string, type?: ToastType) => void;
sessionLive?: boolean;
onTaskUpdated?: (task: Task) => void;
+ expanded?: boolean;
+ onToggleExpanded?: () => void;
}
type AgentLogRole = AgentRole | undefined;
@@ -407,7 +409,7 @@ function TaskChatUserMessage({ message }: { message: UserChatMessage }) {
);
}
-export function TaskChatTab({ task, projectId, active, addToast, sessionLive, onTaskUpdated }: TaskChatTabProps) {
+export function TaskChatTab({ task, projectId, active, addToast, sessionLive, onTaskUpdated, expanded = false, onToggleExpanded }: TaskChatTabProps) {
const { entries, loading } = useAgentLogs(task.id, active, projectId);
const [draft, setDraft] = useState("");
const [sending, setSending] = useState(false);
@@ -601,6 +603,21 @@ export function TaskChatTab({ task, projectId, active, addToast, sessionLive, on
return (
+ {onToggleExpanded ? (
+
+
+
+ ) : null}
(initialTab === "retries" ? "definition" : initialTab);
+ const [chatExpanded, setChatExpanded] = useState(false);
// ── CLI agent session (U11) ────────────────────────────────────────────────
const [cliSession, setCliSession] = useState
(null);
@@ -777,6 +778,13 @@ export function TaskDetailContent({
// Edit mode state
const [isEditing, setIsEditing] = useState(false);
+
+ useEffect(() => {
+ if (activeTab !== "chat" || isEditing) {
+ setChatExpanded(false);
+ }
+ }, [activeTab, isEditing]);
+
const [editTitle, setEditTitle] = useState(task.title || "");
const [editDescription, setEditDescription] = useState(task.description || "");
const [editDependencies, setEditDependencies] = useState(task.dependencies || []);
@@ -2625,6 +2633,7 @@ export function TaskDetailContent({
const autoMergeEnabled = autoMergeEnabledProp ?? (settings?.autoMerge ?? false);
const effectiveAutoMerge = resolveEffectiveAutoMerge({ autoMerge: task.autoMerge }, { autoMerge: autoMergeEnabled });
const isManualPrFlow = mergeStrategy === "pull-request" && !autoMergeEnabled;
+ const isChatExpanded = chatExpanded && activeTab === "chat" && !isEditing;
const isCheckPrStatusAction = isManualPrFlow && !prAutomationLabel && task.prInfo?.status === "open";
let manualReviewActionLabel = t("taskDetail.pr.mergeAndClose", "Merge & Close");
@@ -2640,7 +2649,7 @@ export function TaskDetailContent({
return (
@@ -3144,6 +3153,8 @@ export function TaskDetailContent({
addToast={addToast}
sessionLive={isCliSessionLive(cliSession)}
onTaskUpdated={handleChatTaskUpdated}
+ expanded={chatExpanded}
+ onToggleExpanded={() => setChatExpanded((value) => !value)}
/>
) : activeTab === "logs" ? (
diff --git a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx
index 91bf5a0345..ec30d1ca26 100644
--- a/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TaskChatTab.test.tsx
@@ -276,6 +276,43 @@ describe("TaskChatTab", () => {
expect(screen.getByText(/No agent output yet/)).toBeTruthy();
});
+ it("renders the collapsed expand toggle and calls the toggle handler", () => {
+ const onToggleExpanded = vi.fn();
+ render();
+
+ const toggle = screen.getByTestId("task-chat-expand-toggle");
+ expect(toggle).toHaveAttribute("aria-label", "Expand chat to full modal");
+ expect(toggle).toHaveAttribute("aria-pressed", "false");
+ expect(toggle).toHaveTextContent("Expand");
+
+ fireEvent.click(toggle);
+ expect(onToggleExpanded).toHaveBeenCalledTimes(1);
+ });
+
+ it("renders the expanded collapse toggle", () => {
+ render();
+
+ const toggle = screen.getByTestId("task-chat-expand-toggle");
+ expect(toggle).toHaveAttribute("aria-label", "Collapse chat");
+ expect(toggle).toHaveAttribute("aria-pressed", "true");
+ expect(toggle).toHaveTextContent("Collapse");
+ });
+
+ it("renders the expand toggle while the transcript is loading", () => {
+ mockLogs([], true);
+ render();
+
+ expect(screen.getByTestId("task-chat-expand-toggle")).toBeInTheDocument();
+ expect(screen.getByText("Loading agent output…")).toBeInTheDocument();
+ });
+
+ it("renders the expand toggle in the empty transcript state", () => {
+ render();
+
+ expect(screen.getByTestId("task-chat-expand-toggle")).toBeInTheDocument();
+ expect(screen.getByText(/No agent output yet/)).toBeInTheDocument();
+ });
+
it("labels every agent role and the legacy undefined-agent fallback", () => {
mockLogs([
makeEntry({ agent: "triage", text: "planning output" }),
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 aca0f6b366..d536eb38ee 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
@@ -787,6 +787,106 @@ describe("TaskDetailModal", () => {
expect(mobileSectionRule).toContain("min-height: 0");
});
+ it("FN-6370 defines expanded chat chrome-hiding CSS for desktop and mobile", () => {
+ const css = readDashboardStylesSource();
+ const expandedChromeRule = getCssRuleBlock(css, ".task-detail-content--chat-expanded .detail-title-row");
+ const expandedBodyRule = getCssRuleBlock(css, ".task-detail-content--chat-expanded .detail-body--chat");
+ const expandedSectionRule = getCssRuleBlock(css, ".task-detail-content--chat-expanded .detail-section--chat");
+ const mobileCss = css.slice(css.indexOf("@media (max-width: 768px)"));
+ const mobileTabsRule = getCssRuleBlock(mobileCss, ".task-detail-content--chat-expanded .detail-tabs");
+
+ expect(expandedChromeRule).toContain("display: none");
+ expect(expandedBodyRule).toContain("flex: 1");
+ expect(expandedBodyRule).toContain("min-height: 0");
+ expect(expandedSectionRule).toContain("margin-top: 0");
+ expect(mobileTabsRule).toContain("display: none");
+ });
+
+ it("FN-6370 expands and collapses chat without leaving chrome hidden", () => {
+ const { container } = render(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: "Chat" }));
+ const content = container.querySelector(".task-detail-content");
+ expect(content).not.toHaveClass("task-detail-content--chat-expanded");
+ expect(container.querySelector(".detail-tabs")).toBeTruthy();
+ expect(container.querySelector(".modal-actions")).toBeTruthy();
+
+ fireEvent.click(screen.getByTestId("task-chat-expand-toggle"));
+ expect(content).toHaveClass("task-detail-content--chat-expanded");
+ expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Collapse chat");
+ expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-pressed", "true");
+
+ fireEvent.click(screen.getByTestId("task-chat-expand-toggle"));
+ expect(content).not.toHaveClass("task-detail-content--chat-expanded");
+ expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-label", "Expand chat to full modal");
+ expect(screen.getByTestId("task-chat-expand-toggle")).toHaveAttribute("aria-pressed", "false");
+ });
+
+ it("FN-6370 resets expanded chat when the active tab changes", () => {
+ const { container, rerender } = render(
+ ,
+ );
+
+ const content = container.querySelector(".task-detail-content");
+ fireEvent.click(screen.getByTestId("task-chat-expand-toggle"));
+ expect(content).toHaveClass("task-detail-content--chat-expanded");
+
+ rerender(
+ ,
+ );
+
+ expect(container.querySelector(".task-detail-content--chat-expanded")).toBeNull();
+ expect(screen.queryByTestId("task-chat-expand-toggle")).toBeNull();
+ });
+
+ it("FN-6370 resets expanded chat when entering edit mode", () => {
+ const { container } = render(
+ ,
+ );
+
+ fireEvent.click(screen.getByRole("button", { name: "Chat" }));
+ fireEvent.click(screen.getByTestId("task-chat-expand-toggle"));
+ expect(container.querySelector(".task-detail-content")).toHaveClass("task-detail-content--chat-expanded");
+
+ fireEvent.click(screen.getByLabelText("Edit task"));
+ expect(container.querySelector(".task-detail-content--chat-expanded")).toBeNull();
+ expect(screen.queryByTestId("task-chat-expand-toggle")).toBeNull();
+ });
+
it("FN-6347 applies chat modifiers only while the Chat tab is active", () => {
const { container } = render(