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(