From 17d7bd19eff2ff658abc738b4d1172d71bdbd049 Mon Sep 17 00:00:00 2001 From: gsxdsm Date: Sat, 11 Jul 2026 18:31:14 -0700 Subject: [PATCH] FN-7826: make Task Detail interactive Terminal tab always available Removes the single-worktree gate on the Task Detail Terminal tab so it renders for every task, defaulting its first shell to the task worktree when present and otherwise falling back to the project root. - TaskDetailModal.tsx: showWorktreeTerminalTab is now always true (drops the isWorkspaceTask/single-worktree gate and its stale fallback effect); taskWorktreeCwd still feeds defaultCwd when a worktree is recorded, and the tab renders without requiring taskWorktreeCwd - Adds a changeset documenting the behavior change (minor, feature) for @runfusion/fusion - docs/dashboard-guide.md: updates the Terminal tab description to state it is always available, with worktree-or-project-root cwd fallback, including for multi-repo workspace tasks - Expands TaskDetailModal.worktree-terminal.test.tsx coverage for the no-worktree and workspace-task cases now that the tab is always shown Files changed: .changeset/FN-7826-worktree-terminal-always-available.md | 7 +++ docs/dashboard-guide.md | 4 +- packages/dashboard/app/components/TaskDetailModal.tsx | 12 ++--- .../TaskDetailModal.worktree-terminal.test.tsx | 57 +++++++++++++++++++--- 4 files changed, 63 insertions(+), 17 deletions(-) Fusion-Task-Id: FN-7826 Fusion-Task-Lineage: f87504fe-9330-4392-bdbe-33bba006d96c Co-authored-by: Fusion (runfusion.ai) --- ...7826-worktree-terminal-always-available.md | 7 +++ docs/dashboard-guide.md | 4 +- .../app/components/TaskDetailModal.tsx | 12 ++-- ...TaskDetailModal.worktree-terminal.test.tsx | 57 ++++++++++++++++--- 4 files changed, 63 insertions(+), 17 deletions(-) create mode 100644 .changeset/FN-7826-worktree-terminal-always-available.md diff --git a/.changeset/FN-7826-worktree-terminal-always-available.md b/.changeset/FN-7826-worktree-terminal-always-available.md new file mode 100644 index 0000000000..cb2dd6375b --- /dev/null +++ b/.changeset/FN-7826-worktree-terminal-always-available.md @@ -0,0 +1,7 @@ +--- +"@runfusion/fusion": minor +--- + +summary: The Task Detail Terminal tab is now always available, falling back to the project root when a task has no worktree. +category: feature +dev: Relaxes the TaskDetailModal `showWorktreeTerminalTab` gate to always render and passes `defaultCwd` = worktree when present else undefined (project-root auto-create via useTerminalSessions). Covers no-worktree and multi-repo workspace tasks. Sessions stay task-scoped via `scopeId`. diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md index a03b5a721f..c1c2fdd05f 100644 --- a/docs/dashboard-guide.md +++ b/docs/dashboard-guide.md @@ -625,8 +625,8 @@ Mailbox view shows inbox/outbox communication threads and unread state. Fusion embeds a terminal using xterm.js. Desktop and tablet use the footer status bar as the terminal launcher; mobile keeps the full-screen terminal path. - -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 appears only for non-workspace tasks with a single recorded worktree; its first shell starts in that task worktree, and 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; 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. 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. diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx index 8b43b6013a..966aeae873 100644 --- a/packages/dashboard/app/components/TaskDetailModal.tsx +++ b/packages/dashboard/app/components/TaskDetailModal.tsx @@ -1158,11 +1158,11 @@ export function TaskDetailContent({ ); const showCliTab = cliTabVisibility.kind !== "hidden"; /* - FNXC:TaskDetailTerminal 2026-07-10-00:00: - FN-7813 exposes a dedicated interactive Terminal tab only for single-worktree tasks. Workspace tasks have multiple repo roots and no single safe default shell cwd, so they intentionally hide this tab rather than falling back to the project root. + FNXC:TaskDetailTerminal 2026-07-11-13:20: + FN-7826 makes the interactive Terminal tab always available in Task Detail. The first shell opens in task.worktree when present; otherwise defaultCwd stays undefined so useTerminalSessions creates a project-root shell, including for multi-repo workspace tasks with no single worktree. Terminal sessions remain task-scoped through scopeId so they do not share the footer/global terminal namespace. */ const taskWorktreeCwd = typeof task.worktree === "string" && task.worktree.trim().length > 0 ? task.worktree : undefined; - const showWorktreeTerminalTab = Boolean(taskWorktreeCwd) && !isWorkspaceTask(workingTask); + const showWorktreeTerminalTab = true; const cliPosture: SessionTerminalPosture | undefined = useMemo(() => { if (!cliSession) return undefined; const p = cliSession.autonomyPosture ?? {}; @@ -1197,10 +1197,6 @@ export function TaskDetailContent({ if (activeTab === "terminal" && !showCliTab) setActiveTab("definition"); }, [activeTab, showCliTab]); - // If the worktree-rooted terminal tab loses its single-worktree cwd, fall back. - useEffect(() => { - if (activeTab === "worktree-terminal" && !showWorktreeTerminalTab) setActiveTab("definition"); - }, [activeTab, showWorktreeTerminalTab]); // Track mount state to avoid setting state on unmounted component useEffect(() => { @@ -4858,7 +4854,7 @@ export function TaskDetailContent({ ) : null} - ) : activeTab === "worktree-terminal" && showWorktreeTerminalTab && taskWorktreeCwd ? ( + ) : activeTab === "worktree-terminal" && showWorktreeTerminalTab ? (
}> { expect(await screen.findByRole("button", { name: "Terminal" })).toBeInTheDocument(); }); - it("hides the interactive Terminal tab when no worktree exists", async () => { + it("shows the interactive Terminal tab when no worktree exists", async () => { renderDetail(makeTask({ id: "FN-7813", worktree: undefined })); - await waitFor(() => expect(screen.queryByRole("button", { name: "Terminal" })).toBeNull()); + expect(await screen.findByRole("button", { name: "Terminal" })).toBeInTheDocument(); }); - it("hides the interactive Terminal tab for workspace tasks without a singular worktree", async () => { + it("shows the interactive Terminal tab for workspace tasks without a singular worktree", async () => { renderDetail(makeTask({ id: "FN-7813", worktree: undefined, @@ -74,10 +74,10 @@ describe("TaskDetailModal worktree terminal tab", () => { }, })); - await waitFor(() => expect(screen.queryByRole("button", { name: "Terminal" })).toBeNull()); + expect(await screen.findByRole("button", { name: "Terminal" })).toBeInTheDocument(); }); - it("falls back from the active worktree terminal tab when eligibility drops", async () => { + 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" })); @@ -98,9 +98,16 @@ describe("TaskDetailModal worktree terminal tab", () => { ); await waitFor(() => { - expect(screen.getByRole("button", { name: "Plan" })).toHaveClass("detail-tab-active"); + expect(screen.getByRole("button", { name: "Terminal" })).toHaveClass("detail-tab-active"); + expect(screen.getByTestId("mock-worktree-terminal")).toBeInTheDocument(); }); - expect(screen.queryByTestId("mock-worktree-terminal")).toBeNull(); + 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 () => { @@ -118,6 +125,42 @@ describe("TaskDetailModal worktree terminal tab", () => { })); }); + 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("renders distinct Session and Terminal tab labels when an agent session exists", async () => { vi.mocked(dashboardApi.api).mockResolvedValueOnce({ sessions: [{