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) <noreply@runfusion.ai>
This commit is contained in:
gsxdsm
2026-07-11 18:31:14 -07:00
parent ee5c2a80e6
commit 17d7bd19ef
4 changed files with 63 additions and 17 deletions

View File

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

View File

@@ -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.
<!-- FNXC:TaskDetailTerminal 2026-07-10-00:00: FN-7813 gives single-worktree tasks their own Task Detail Terminal tab while preserving the existing CLI-agent Session tab label, so operators can distinguish an interactive shell from the read-only/live agent session transcript. -->
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.
<!-- 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.
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.

View File

@@ -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({
</Suspense>
) : null}
</div>
) : activeTab === "worktree-terminal" && showWorktreeTerminalTab && taskWorktreeCwd ? (
) : activeTab === "worktree-terminal" && showWorktreeTerminalTab ? (
<div className="detail-section detail-section--worktree-terminal">
<Suspense fallback={<div className="detail-loading"><LoadingSpinner label={t("taskDetail.terminal.loadingInteractive", "Loading interactive terminal…")} /></div>}>
<LazyTerminalModal

View File

@@ -59,13 +59,13 @@ describe("TaskDetailModal worktree terminal tab", () => {
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: [{