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>
195 lines
6.4 KiB
TypeScript
195 lines
6.4 KiB
TypeScript
import React from "react";
|
|
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
import { render, screen, fireEvent, waitFor } from "@testing-library/react";
|
|
|
|
vi.mock("mermaid", () => ({
|
|
default: {
|
|
initialize: vi.fn(),
|
|
render: vi.fn().mockResolvedValue({ svg: "<svg data-testid='mock-mermaid-svg'></svg>" }),
|
|
},
|
|
}));
|
|
|
|
const mockEmbeddedTerminal = vi.fn((props: Record<string, unknown>) => (
|
|
<div data-testid="mock-worktree-terminal" data-props={JSON.stringify(props)} />
|
|
));
|
|
|
|
vi.mock("../TerminalModal", () => ({
|
|
TerminalModal: (props: Record<string, unknown>) => mockEmbeddedTerminal(props),
|
|
}));
|
|
|
|
import {
|
|
makeTask,
|
|
noop,
|
|
noopDelete,
|
|
noopMerge,
|
|
noopMove,
|
|
noopOpenDetail,
|
|
setupTaskDetailModalHooks,
|
|
} from "./TaskDetailModal.test-helpers";
|
|
import { TaskDetailModal } from "../TaskDetailModal";
|
|
import * as dashboardApi from "../../api";
|
|
|
|
setupTaskDetailModalHooks();
|
|
|
|
function renderDetail(task = makeTask({ id: "FN-7813", worktree: "/repo/.worktrees/FN-7813" }), initialTab: "definition" | "worktree-terminal" = "definition") {
|
|
return render(
|
|
<TaskDetailModal
|
|
initialTab={initialTab}
|
|
task={task}
|
|
projectId="proj-123"
|
|
onClose={noop}
|
|
onMoveTask={noopMove}
|
|
onDeleteTask={noopDelete}
|
|
onMergeTask={noopMerge}
|
|
onOpenDetail={noopOpenDetail}
|
|
addToast={noop}
|
|
/>,
|
|
);
|
|
}
|
|
|
|
describe("TaskDetailModal worktree terminal tab", () => {
|
|
beforeEach(() => {
|
|
mockEmbeddedTerminal.mockClear();
|
|
vi.spyOn(dashboardApi, "api").mockResolvedValue({ sessions: [] });
|
|
});
|
|
|
|
it("shows the interactive Terminal tab when a single-task worktree exists", async () => {
|
|
renderDetail();
|
|
|
|
expect(await screen.findByRole("button", { name: "Terminal" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows the interactive Terminal tab when no worktree exists", async () => {
|
|
renderDetail(makeTask({ id: "FN-7813", worktree: undefined }));
|
|
|
|
expect(await screen.findByRole("button", { name: "Terminal" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("shows the interactive Terminal tab for workspace tasks without a singular worktree", async () => {
|
|
renderDetail(makeTask({
|
|
id: "FN-7813",
|
|
worktree: undefined,
|
|
workspaceWorktrees: {
|
|
"packages/app": { worktreePath: "/repo/.worktrees/FN-7813-app", branch: "fusion/FN-7813-app" },
|
|
},
|
|
}));
|
|
|
|
expect(await screen.findByRole("button", { name: "Terminal" })).toBeInTheDocument();
|
|
});
|
|
|
|
it("keeps the active Terminal tab visible when the worktree disappears and falls back to the project root", async () => {
|
|
const { rerender } = renderDetail(undefined, "worktree-terminal");
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: "Terminal" }));
|
|
expect(await screen.findByTestId("mock-worktree-terminal")).toBeInTheDocument();
|
|
|
|
rerender(
|
|
<TaskDetailModal
|
|
initialTab="worktree-terminal"
|
|
task={makeTask({ id: "FN-7813", worktree: undefined })}
|
|
projectId="proj-123"
|
|
onClose={noop}
|
|
onMoveTask={noopMove}
|
|
onDeleteTask={noopDelete}
|
|
onMergeTask={noopMerge}
|
|
onOpenDetail={noopOpenDetail}
|
|
addToast={noop}
|
|
/>,
|
|
);
|
|
|
|
await waitFor(() => {
|
|
expect(screen.getByRole("button", { name: "Terminal" })).toHaveClass("detail-tab-active");
|
|
expect(screen.getByTestId("mock-worktree-terminal")).toBeInTheDocument();
|
|
});
|
|
expect(mockEmbeddedTerminal).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
embedded: true,
|
|
isOpen: true,
|
|
defaultCwd: undefined,
|
|
scopeId: "FN-7813",
|
|
projectId: "proj-123",
|
|
}));
|
|
});
|
|
|
|
it("passes the task worktree and task-scoped namespace into embedded TerminalModal", async () => {
|
|
renderDetail(undefined, "worktree-terminal");
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: "Terminal" }));
|
|
await screen.findByTestId("mock-worktree-terminal");
|
|
|
|
expect(mockEmbeddedTerminal).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
embedded: true,
|
|
isOpen: true,
|
|
defaultCwd: "/repo/.worktrees/FN-7813",
|
|
scopeId: "FN-7813",
|
|
projectId: "proj-123",
|
|
}));
|
|
});
|
|
|
|
it("passes undefined cwd for no-worktree tasks so TerminalModal creates a project-root shell", async () => {
|
|
renderDetail(makeTask({ id: "FN-7826", worktree: undefined }), "worktree-terminal");
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: "Terminal" }));
|
|
await screen.findByTestId("mock-worktree-terminal");
|
|
|
|
expect(mockEmbeddedTerminal).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
embedded: true,
|
|
isOpen: true,
|
|
defaultCwd: undefined,
|
|
scopeId: "FN-7826",
|
|
projectId: "proj-123",
|
|
}));
|
|
});
|
|
|
|
it("passes undefined cwd for workspace tasks so TerminalModal creates a project-root shell", async () => {
|
|
renderDetail(makeTask({
|
|
id: "FN-7826",
|
|
worktree: undefined,
|
|
workspaceWorktrees: {
|
|
"packages/app": { worktreePath: "/repo/.worktrees/FN-7826-app", branch: "fusion/FN-7826-app" },
|
|
},
|
|
}), "worktree-terminal");
|
|
|
|
fireEvent.click(await screen.findByRole("button", { name: "Terminal" }));
|
|
await screen.findByTestId("mock-worktree-terminal");
|
|
|
|
expect(mockEmbeddedTerminal).toHaveBeenLastCalledWith(expect.objectContaining({
|
|
embedded: true,
|
|
isOpen: true,
|
|
defaultCwd: undefined,
|
|
scopeId: "FN-7826",
|
|
projectId: "proj-123",
|
|
}));
|
|
});
|
|
|
|
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: [{
|
|
id: "cli-1",
|
|
taskId: "FN-7813",
|
|
projectId: "proj-123",
|
|
adapterId: "claude-local",
|
|
agentState: "busy",
|
|
terminationReason: null,
|
|
autonomyPosture: null,
|
|
}],
|
|
});
|
|
|
|
renderDetail();
|
|
|
|
expect(await screen.findByRole("button", { name: "Session" })).toBeInTheDocument();
|
|
expect(screen.getByRole("button", { name: "Terminal" })).toBeInTheDocument();
|
|
});
|
|
});
|