FN-7832: reorder Task Detail tabs and default terminal picker to task worktree

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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-11 20:46:05 -07:00
parent fc07bdfc7e
commit 49faf0afe2
8 changed files with 134 additions and 27 deletions

View File

@@ -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`.

View File

@@ -631,8 +631,9 @@ Fusion embeds a terminal using xterm.js. Desktop and tablet use the footer statu
<!-- FNXC:Terminal 2026-07-11-18:20: FN-7824 first-launch terminal sockets auto-retry with capped backoff until the first successful open, so the manual Reconnect affordance is reserved for terminal sessions that already connected and then exhaust their mid-session reconnect budget. -->
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.
<!-- FNXC:TaskDetailTerminal 2026-07-11-13:20: FN-7826 makes the Task Detail interactive Terminal tab always available while preserving the existing CLI-agent Session tab label. The first shell uses task.worktree when present and otherwise falls back to the project base directory, including for multi-repo workspace tasks with no single worktree, while task-scoped terminal tabs remain separate from the footer/global project terminal. -->
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.
<!-- FNXC:TaskDetailTerminal 2026-07-11-13:20: FN-7826 makes the Task Detail interactive Terminal tab always available while preserving the existing CLI-agent Session tab label. The first shell uses task.worktree when present and otherwise falls back to the project base directory, including for multi-repo workspace tasks with no single worktree, while task-scoped terminal tabs remain separate from the footer/global project terminal.
FNXC:TaskDetailTerminal 2026-07-11-00:00: Task Detail's collaboration strip groups Comments → Terminal → Cost, and the embedded terminal picker mirrors the task worktree when that worktree is registered so operators can see the cwd context; the global footer terminal still defaults to Project Root. -->
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`.
<!-- FNXC:TaskDetailCost 2026-07-11-12:25: Cost is read-time derived from task token usage and model pricing overrides, so the always-available Cost tab documents spend without adding persistence or hiding unavailable pricing behind a false zero. -->
- 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`.
<!-- FNXC:TaskDetailCost 2026-07-11-00:00: Cost is read-time derived from task token usage and model pricing overrides, so the always-available Cost tab documents spend without adding persistence or hiding unavailable pricing behind a false zero. Keep it adjacent to Task Detail Terminal so shell context and spend context stay together after Comments. -->
- 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.

View File

@@ -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));
}
}

View File

@@ -4380,13 +4380,6 @@ export function TaskDetailContent({
{t("taskDetail.tabs.summary", "Summary")}
</button>
)}
{/* 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. */}
<button
className={`detail-tab${activeTab === "cost" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("cost")}
>
{t("taskDetail.tabs.cost", "Cost")}
</button>
<button
className={`detail-tab${activeTab === "definition" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("definition")}
@@ -4421,6 +4414,21 @@ export function TaskDetailContent({
>
{t("taskDetail.tabs.comments", "Comments")}
</button>
{/* 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 && (
<button
className={`detail-tab${activeTab === "worktree-terminal" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("worktree-terminal")}
>
{t("taskDetail.tabs.worktreeTerminal", "Terminal")}
</button>
)}
<button
className={`detail-tab${activeTab === "cost" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("cost")}
>
{t("taskDetail.tabs.cost", "Cost")}
</button>
<button
className={`detail-tab${activeTab === "documents" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("documents")}
@@ -4460,14 +4468,6 @@ export function TaskDetailContent({
{t("taskDetail.tabs.terminal", "Session")}
</button>
)}
{showWorktreeTerminalTab && (
<button
className={`detail-tab${activeTab === "worktree-terminal" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("worktree-terminal")}
>
{t("taskDetail.tabs.worktreeTerminal", "Terminal")}
</button>
)}
{/* Plugin tabs */}
{pluginTabs.map(({ entry, tabId }) => {
return (

View File

@@ -1162,6 +1162,13 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
const [terminalWorkspaceMenuOpen, setTerminalWorkspaceMenuOpen] = useState(false);
const [terminalWorkspaceMenuPosition, setTerminalWorkspaceMenuPosition] = useState<TerminalWorkspaceMenuPosition | null>(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);
}}

View File

@@ -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

View File

@@ -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<HTMLButtonElement>(".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: [{

View File

@@ -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(
<TerminalModal
isOpen={true}
onClose={mockOnClose}
embedded
defaultCwd="/repo/.worktrees/FN-7832"
scopeId="FN-7832"
/>,
);
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(
<TerminalModal
isOpen={true}
onClose={mockOnClose}
embedded
defaultCwd="/repo/.worktrees/FN-7832"
scopeId="FN-7832"
/>,
);
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(<TerminalModal isOpen={true} onClose={mockOnClose} />);
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({