From 49faf0afe2076cc216d80161965c90c386c6d92a Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 11 Jul 2026 20:46:05 -0700 Subject: [PATCH] FN-7832: reorder Task Detail tabs and default terminal picker to task worktree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reworks the Task Detail terminal experience: the embedded Terminal tab now sits between Comments and Cost, its workspace picker defaults to the task's worktree, and the mobile terminal panel is shorter. - Move the Terminal tab in Task Detail's tab strip to sit right after Comments and before Cost (previously Cost was earlier and Terminal was near the Session tab) - TerminalModal now defaults its workspace picker to the useWorkspaces entry whose worktree matches the passed defaultCwd, but only until the operator manually changes the selection; the footer/global terminal still defaults to Project Root since it doesn't pass defaultCwd - Reduce `.detail-section--worktree-terminal`'s mobile min-height from min(65dvh, 14 * --space-2xl) to min(50dvh, 11 * --space-2xl) so tab context and controls stay reachable above the fold - Update docs/dashboard-guide.md to describe the new Comments → Terminal → Cost tab order and the worktree-matching picker default - Add regression tests covering the new tab order and the default terminal workspace selection behavior - Add a patch changeset describing the user-facing change Files changed: .changeset/FN-7832-task-terminal-picker-and-tab-order.md | 7 +++ docs/dashboard-guide.md | 9 +-- packages/dashboard/app/components/TaskDetailModal.css | 5 +- packages/dashboard/app/components/TaskDetailModal.tsx | 30 +++++----- packages/dashboard/app/components/TerminalModal.tsx | 19 +++++++ packages/dashboard/app/components/__tests__/TaskDetailModal.attachments-and-tabs.test.tsx | 15 ++--- packages/dashboard/app/components/__tests__/TaskDetailModal.worktree-terminal.test.tsx | 12 ++++ packages/dashboard/app/components/__tests__/TerminalModal.test.tsx | 64 ++++++++++++++++++++++ 8 files changed, 134 insertions(+), 27 deletions(-) Fusion-Task-Id: FN-7832 Fusion-Task-Lineage: 4ee67a65-8564-49c9-b93c-8c3eab05c073 Co-authored-by: Fusion (runfusion.ai) --- ...7832-task-terminal-picker-and-tab-order.md | 7 ++ docs/dashboard-guide.md | 9 +-- .../app/components/TaskDetailModal.css | 5 +- .../app/components/TaskDetailModal.tsx | 30 ++++----- .../app/components/TerminalModal.tsx | 19 ++++++ ...kDetailModal.attachments-and-tabs.test.tsx | 15 +++-- ...TaskDetailModal.worktree-terminal.test.tsx | 12 ++++ .../__tests__/TerminalModal.test.tsx | 64 +++++++++++++++++++ 8 files changed, 134 insertions(+), 27 deletions(-) create mode 100644 .changeset/FN-7832-task-terminal-picker-and-tab-order.md diff --git a/.changeset/FN-7832-task-terminal-picker-and-tab-order.md b/.changeset/FN-7832-task-terminal-picker-and-tab-order.md new file mode 100644 index 0000000000..8bfad9be5a --- /dev/null +++ b/.changeset/FN-7832-task-terminal-picker-and-tab-order.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": patch +--- + +summary: Task Detail terminal now shows its worktree, is shorter on mobile, and sits with Cost after Comments. +category: feature +dev: TerminalModal defaults its workspace picker to the useWorkspaces entry matching `defaultCwd` (embedded task terminal only; footer terminal stays on Project Root). TaskDetailModal reorders the tab strip to Comments → Terminal → Cost and reduces the mobile min-height of `.detail-section--worktree-terminal`. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index 4144b6898b..d41d67d78c 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -631,8 +631,9 @@ Fusion embeds a terminal using xterm.js. Desktop and tablet use the footer statu On first launch or first open, the terminal keeps reconnecting automatically until its initial WebSocket opens; it should show **Reconnecting...** during that cold-start recovery rather than requiring a manual **Reconnect** click. If an already-connected terminal drops and exhausts its bounded reconnect budget, Fusion then parks it as **Disconnected** and surfaces the manual **Reconnect** control. - -Task Detail has two terminal-adjacent tabs when both are applicable: **Session** shows the pre-existing CLI agent session transcript/control surface, while **Terminal** embeds the interactive multi-tab terminal inside the task detail body. The interactive **Terminal** tab is always available in Task Detail; its first shell starts in the task worktree when one is recorded, otherwise it starts in the project base directory (project root), including for multi-repo workspace tasks that have no single task worktree. Its terminal tabs are stored separately from the footer/global project terminal tabs. + +Task Detail has two terminal-adjacent tabs when both are applicable: **Session** shows the pre-existing CLI agent session transcript/control surface, while **Terminal** embeds the interactive multi-tab terminal inside the task detail body. The interactive **Terminal** tab is always available in Task Detail and sits immediately after **Comments**, with **Cost** immediately after **Terminal**. Its first shell starts in the task worktree when one is recorded, otherwise it starts in the project base directory (project root), including for multi-repo workspace tasks that have no single task worktree. When that task worktree is registered in the workspace picker, the picker shows the task worktree instead of **Project Root**; the footer/global project terminal keeps its separate Project Root default. Task-detail terminal tabs are stored separately from the footer/global project terminal tabs. On Windows, the embedded terminal starts a supported shell inside Fusion, such as Command Prompt (`cmd.exe`) or Windows PowerShell. Windows Terminal (`wt.exe`) is an external terminal host and is not required or launched for the embedded panel, so Fusion should not show native Windows Terminal help/version popups while starting a terminal. If embedded terminal startup fails, Fusion shows an inline error with **Retry** instead of a blocking native dialog; install or repair Windows Terminal separately with `winget install Microsoft.WindowsTerminal` only if you want to use Windows Terminal outside Fusion. @@ -1247,8 +1248,8 @@ Inspect task definition, logs, review feedback, comments, artifacts, workflow ou - The top-level **Chat** tab appears first for active task details and is the default landing tab for non-`done` tasks. It uses the task's effective planning model, but opening the tab is lookup-only: Fusion creates the task-scoped planner chat only after you send a composer message, starter prompt, or planner-question answer. Once a user message exists, the resumable planner chat can appear in the global Chat list; interacted chats are kept when the task reaches `done` and removed when the task is archived. Each send includes bounded server-built task context so the planner can answer current status, progress, recent activity, dependency, and task definition questions. It shows starter prompts for common planning questions, can render structured planner questions, and converts only explicit operator steering intent through the scoped steering tool. The composer stays pinned while the transcript, loading, error, starter, history, and streaming states scroll internally; on mobile/narrow task detail, the default focused Chat layout hides nonessential title/metadata/tab/action rows until you collapse it from the in-view expand control. - The **Activity → Live**, **Feed**, and **Raw Logs** segments remain immediately after **Chat** and share an expand/collapse control that lets the active Activity segment fill the task-detail modal, then restores the normal header, tabs, and action footer when collapsed. - The **Summary** tab appears for `done` tasks and remains their default landing tab. It shows the recorded completion summary, changed-file/merge stats when available, completed steps, workflow results, retry counts, and a token usage & cost section broken down by model from the already-loaded task detail; unpriced models show cost as unavailable rather than `$0`. -- The **Cost** tab is available for tasks in every column. It shows the read-time derived per-model cost breakdown (input, output, cached, cache-write, total tokens, derived USD) and a task total; no token usage shows an explicit empty state, while unpriced or zero-usage rows use `—` instead of a guessed `$0`. - +- The **Cost** tab is available for tasks in every column and sits immediately after **Comments → Terminal** in the tab strip. It shows the read-time derived per-model cost breakdown (input, output, cached, cache-write, total tokens, derived USD) and a task total; no token usage shows an explicit empty state, while unpriced or zero-usage rows use `—` instead of a guessed `$0`. + - Task-detail Activity steering comments are persisted as user comments/steering guidance and surfaced to every relevant agent lane: live executor sessions receive steering injection, while planner, reviewer (spec/plan/code), and merger agents (standard and clean-room AI merge/review) receive the latest user comments in their next prompt/pass. - 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. diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css index 4820adf0ed..37eb305c10 100644 --- a/packages/dashboard/app/components/TaskDetailModal.css +++ b/packages/dashboard/app/components/TaskDetailModal.css @@ -3681,6 +3681,9 @@ Read-only list/placeholder only — not the deferred rich per-repo-status compon /* FNXC:TaskDetailTerminal 2026-07-10-00:00: FN-7813's interactive task Terminal tab hosts an embedded TerminalModal, so the section must provide a real flex height and keep terminal scrolling internal to the shell instead of the surrounding detail modal. + +FNXC:TaskDetailTerminal 2026-07-11-00:00: +Mobile Task Detail needs a shorter embedded terminal than the desktop/tablet shell so tab context and controls remain reachable above the fold; keep the value token-based and large enough for an interactive shell. */ .detail-section--worktree-terminal { display: flex; @@ -3691,6 +3694,6 @@ FN-7813's interactive task Terminal tab hosts an embedded TerminalModal, so the @media (max-width: 768px) { .detail-section--worktree-terminal { - min-height: min(65dvh, calc(var(--space-2xl) * 14)); + min-height: min(50dvh, calc(var(--space-2xl) * 11)); } } diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index 966aeae873..1fc81e7b89 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -4380,13 +4380,6 @@ export function TaskDetailContent({ {t("taskDetail.tabs.summary", "Summary")} )} - {/* FNXC:TaskDetailCost 2026-07-11-12:10: The Cost tab is always reachable (unlike done-only Summary) because operators need read-time model spend visibility while work is still in progress; it reuses costFor via the shared taskTokenCost helper and never persists derived USD. */} - + {/* FNXC:TaskDetailCost 2026-07-11-00:00: Keep the tab strip's operator workflow as Comments → Terminal → Cost so discussion, shell context, and model spend sit together. Cost remains always reachable (unlike done-only Summary) and uses costFor via the shared taskTokenCost helper without persisting derived USD. */} + {showWorktreeTerminalTab && ( + + )} + )} - {showWorktreeTerminalTab && ( - - )} {/* Plugin tabs */} {pluginTabs.map(({ entry, tabId }) => { return ( diff --git a/packages/dashboard/app/components/TerminalModal.tsx b/packages/dashboard/app/components/TerminalModal.tsx index 578ee1c71e..4a91f5a4df 100644 --- a/packages/dashboard/app/components/TerminalModal.tsx +++ b/packages/dashboard/app/components/TerminalModal.tsx @@ -1162,6 +1162,13 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG const [terminalWorkspaceMenuOpen, setTerminalWorkspaceMenuOpen] = useState(false); const [terminalWorkspaceMenuPosition, setTerminalWorkspaceMenuPosition] = useState(null); const [selectedTerminalWorkspaceId, setSelectedTerminalWorkspaceId] = useState("project"); + const terminalWorkspaceSelectionTouchedRef = useRef(false); + const defaultTerminalWorkspaceId = useMemo(() => { + if (typeof defaultCwd !== "string" || defaultCwd.trim().length === 0) { + return undefined; + } + return terminalWorkspaces.find((workspace) => workspace.worktree && workspace.worktree === defaultCwd)?.id; + }, [defaultCwd, terminalWorkspaces]); const selectedTerminalWorkspace = useMemo( () => terminalWorkspaces.find((workspace) => workspace.id === selectedTerminalWorkspaceId) ?? null, @@ -1180,7 +1187,17 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG FNXC:TerminalWorkspaces 2026-06-29-00:00: The picker is a header menu, not terminal input: Escape and outside clicks close the listbox first so users do not accidentally close the whole terminal while navigating worktrees with keyboard or touch. + + FNXC:TerminalWorkspaces 2026-07-11-00:00: + Embedded Task Detail terminals pass defaultCwd for the first shell, so default the picker to the registered workspace whose worktree exactly matches that path. Apply this only until the operator manually changes the picker; footer terminals omit defaultCwd and continue to show Project Root. */ + useEffect(() => { + if (!defaultTerminalWorkspaceId || selectedTerminalWorkspaceId !== "project" || terminalWorkspaceSelectionTouchedRef.current) { + return; + } + setSelectedTerminalWorkspaceId(defaultTerminalWorkspaceId); + }, [defaultTerminalWorkspaceId, selectedTerminalWorkspaceId]); + useEffect(() => { if (selectedTerminalWorkspaceId === "project") { return; @@ -2648,6 +2665,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG type="button" className={`terminal-workspace-picker-option${selectedTerminalWorkspaceId === "project" ? " active" : ""}`} onClick={() => { + terminalWorkspaceSelectionTouchedRef.current = true; setSelectedTerminalWorkspaceId("project"); setTerminalWorkspaceMenuOpen(false); }} @@ -2674,6 +2692,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG className={`terminal-workspace-picker-option${selectedTerminalWorkspaceId === workspace.id ? " active" : ""}${disabled ? " disabled" : ""}`} onClick={() => { if (disabled) return; + terminalWorkspaceSelectionTouchedRef.current = true; setSelectedTerminalWorkspaceId(workspace.id); setTerminalWorkspaceMenuOpen(false); }} 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 a2cbd41198..9858dd4308 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 @@ -856,21 +856,22 @@ describe("TaskDetailModal", () => { ); // For an in-progress task (no workflow steps, no merge commit), the - // top-level tabs are: Activity, Chat, Cost, Plan, Changes, Review, Comments, - // Artifacts, Model, Workflow, Stats, Routing. - const tabTexts = ["Activity", "Chat", "Cost", "Plan", "Changes", "Review", "Comments", "Artifacts", "Model", "Workflow", "Stats", "Routing"]; + // top-level tabs are: Activity, Chat, Plan, Changes, Review, Comments, + // Terminal, Cost, Artifacts, Model, Workflow, Stats, Routing. + const tabTexts = ["Activity", "Chat", "Plan", "Changes", "Review", "Comments", "Terminal", "Cost", "Artifacts", "Model", "Workflow", "Stats", "Routing"]; const tabs = screen.getAllByRole("button").filter((b) => tabTexts.includes(b.textContent || "") ); expect(tabs.map((tab) => tab.textContent)).toEqual(tabTexts); expect(tabs[0].textContent).toBe("Activity"); expect(tabs[1].textContent).toBe("Chat"); - expect(tabs[2].textContent).toBe("Cost"); - expect(tabs[3].textContent).toBe("Plan"); - expect(tabs[4].textContent).toBe("Changes"); + expect(tabs[2].textContent).toBe("Plan"); + expect(tabs[5].textContent).toBe("Comments"); + expect(tabs[6].textContent).toBe("Terminal"); + expect(tabs[7].textContent).toBe("Cost"); expect(screen.queryByRole("button", { name: "Logs" })).toBeNull(); - expect(container.querySelectorAll(".detail-tab").length).toBe(12); + expect(container.querySelectorAll(".detail-tab").length).toBe(13); // Workflow tab should always appear even when no workflow steps are configured expect(screen.getByText("Workflow")).toBeInTheDocument(); // Commits tab should NOT appear for non-done tasks diff --git a/packages/dashboard/app/components/__tests__/TaskDetailModal.worktree-terminal.test.tsx b/packages/dashboard/app/components/__tests__/TaskDetailModal.worktree-terminal.test.tsx index 51f1fca2cb..7a480d0048 100644 --- a/packages/dashboard/app/components/__tests__/TaskDetailModal.worktree-terminal.test.tsx +++ b/packages/dashboard/app/components/__tests__/TaskDetailModal.worktree-terminal.test.tsx @@ -161,6 +161,18 @@ describe("TaskDetailModal worktree terminal tab", () => { })); }); + it("orders Comments, Terminal, and Cost tabs together", async () => { + const { container } = renderDetail(); + + await screen.findByRole("button", { name: "Terminal" }); + const tabLabels = Array.from(container.querySelectorAll(".detail-tabs .detail-tab")) + .map((tab) => tab.textContent?.trim()); + + expect(tabLabels.indexOf("Comments")).toBeGreaterThanOrEqual(0); + expect(tabLabels.indexOf("Terminal")).toBeGreaterThan(tabLabels.indexOf("Comments")); + expect(tabLabels.indexOf("Cost")).toBe(tabLabels.indexOf("Terminal") + 1); + }); + it("renders distinct Session and Terminal tab labels when an agent session exists", async () => { vi.mocked(dashboardApi.api).mockResolvedValueOnce({ sessions: [{ diff --git a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx index 3563c83941..3b92d3c5b6 100644 --- a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx +++ b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx @@ -497,6 +497,70 @@ describe("TerminalModal", () => { expect(defaultSessionState.createTab).toHaveBeenCalledWith(); }); + it("defaults the embedded picker to the workspace matching defaultCwd", async () => { + mockUseWorkspaces.mockReturnValue({ + projectName: "kb", + workspaces: [ + { id: "FN-7832", label: "FN-7832", title: "Task terminal picker", worktree: "/repo/.worktrees/FN-7832", kind: "task" }, + ], + loading: false, + error: null, + }); + + render( + , + ); + + await waitFor(() => { + expect(screen.getByLabelText("Select terminal workspace: FN-7832")).toBeInTheDocument(); + }); + expect(screen.queryByLabelText("Select terminal workspace: Project Root")).toBeNull(); + }); + + it("falls back to Project Root when embedded defaultCwd has no workspace match", async () => { + mockUseWorkspaces.mockReturnValue({ + projectName: "kb", + workspaces: [ + { id: "FN-0001", label: "FN-0001", title: "Different task", worktree: "/repo/.worktrees/FN-0001", kind: "task" }, + ], + loading: false, + error: null, + }); + + render( + , + ); + + expect(await screen.findByLabelText("Select terminal workspace: Project Root")).toBeInTheDocument(); + }); + + it("keeps the footer picker defaulted to Project Root when defaultCwd is omitted", async () => { + mockUseWorkspaces.mockReturnValue({ + projectName: "kb", + workspaces: [ + { id: "FN-7832", label: "FN-7832", title: "Task terminal picker", worktree: "/repo/.worktrees/FN-7832", kind: "task" }, + ], + loading: false, + error: null, + }); + + render(); + + expect(await screen.findByLabelText("Select terminal workspace: Project Root")).toBeInTheDocument(); + }); + it("opens a new terminal in the selected task worktree", async () => { const createTab = vi.fn().mockResolvedValue(defaultTab); mockUseTerminalSessions.mockReturnValue({