diff --git a/.changeset/FN-7813-worktree-terminal-tab.md b/.changeset/FN-7813-worktree-terminal-tab.md
new file mode 100644
index 0000000000..ca330a9eb0
--- /dev/null
+++ b/.changeset/FN-7813-worktree-terminal-tab.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": minor
+---
+
+summary: Add an interactive worktree-rooted Terminal tab to the task detail view.
+category: feature
+dev: TaskDetailModal embeds TerminalModal in a new `embedded` mode; useTerminalSessions gains task-scoped storage + defaultCwd. The pre-existing agent-session tab is relabeled "Session".
diff --git a/AGENTS.md b/AGENTS.md
index 0726759a11..5815d029b2 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -290,7 +290,7 @@ Keep this AGENTS inventory in sync with App lazy imports, AppModals lazy modal i
- `PiExtensionsManager`
- `AgentDetailView`
-Note: the embedded main-content views Workflows (`_WorkflowEditorView`), Import Tasks (`_ImportTasksView`), Automations (`_AutomationsView`), and Settings (`_SettingsView`) in App.tsx are `_`-prefixed lazy splits that reuse already-documented chunks. Task session terminals, onboarding-internal modals, duplicate `AgentDetailView` imports, and right-dock overflow re-imports of already-counted views are also intentionally excluded. These exclusions stay out of the curated list and count; `lazy-loaded-views-docs.test.ts` asserts them explicitly, so do not add them as bullets.
+Note: the embedded main-content views Workflows (`_WorkflowEditorView`), Import Tasks (`_ImportTasksView`), Automations (`_AutomationsView`), and Settings (`_SettingsView`) in App.tsx are `_`-prefixed lazy splits that reuse already-documented chunks. Task session terminals, the Task Detail embedded terminal, onboarding-internal modals, duplicate `AgentDetailView` imports, and right-dock overflow re-imports of already-counted views are also intentionally excluded. These exclusions stay out of the curated list and count; `lazy-loaded-views-docs.test.ts` asserts them explicitly, so do not add them as bullets.
## FNXC_LOG comments:
- Please whenever you're working on a codebase. I want you to add comments describing the date of the change (must be in this format yyyy-MM-dd-hh:mm) and describing the requirements or the change in requirements that made you implement certain functionality.
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index 03ad744e3b..a4b941b46b 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -623,6 +623,9 @@ 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.
+
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.
Use the terminal on desktop/tablet:
diff --git a/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts b/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts
index 43f09401a8..0124499331 100644
--- a/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts
+++ b/packages/dashboard/app/__tests__/lazy-loaded-views-docs.test.ts
@@ -95,8 +95,8 @@ const EXPECTED_EXCLUDED_LAZY = [
},
{
file: "../components/TaskDetailModal.tsx",
- symbols: ["LazySessionTerminal"],
- reason: "task session terminal, not a top-level heavy view or modal",
+ symbols: ["LazySessionTerminal", "LazyTerminalModal"],
+ reason: "task-detail-internal terminal surfaces, not top-level heavy views or modals",
},
{
file: "../components/TaskDetailModal.tsx",
diff --git a/packages/dashboard/app/components/TaskDetailModal.css b/packages/dashboard/app/components/TaskDetailModal.css
index cb3638a937..4820adf0ed 100644
--- a/packages/dashboard/app/components/TaskDetailModal.css
+++ b/packages/dashboard/app/components/TaskDetailModal.css
@@ -3677,3 +3677,20 @@ Read-only list/placeholder only — not the deferred rich per-repo-status compon
.workspace-worktrees-branch {
color: var(--text-muted, inherit);
}
+
+/*
+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.
+*/
+.detail-section--worktree-terminal {
+ display: flex;
+ flex-direction: column;
+ min-height: min(60dvh, calc(var(--space-2xl) * 16));
+ overflow: hidden;
+}
+
+@media (max-width: 768px) {
+ .detail-section--worktree-terminal {
+ min-height: min(65dvh, calc(var(--space-2xl) * 14));
+ }
+}
diff --git a/packages/dashboard/app/components/TaskDetailModal.tsx b/packages/dashboard/app/components/TaskDetailModal.tsx
index 2cf3e3c968..332635880a 100644
--- a/packages/dashboard/app/components/TaskDetailModal.tsx
+++ b/packages/dashboard/app/components/TaskDetailModal.tsx
@@ -226,7 +226,7 @@ function formatDurationCompact(ageMs: number): string {
return `${minutes}m`;
}
-type TabId = "summary" | "definition" | "chat" | "planner-chat" | "logs" | "changes" | "review" | "pr" | "comments" | "model" | "workflow" | "documents" | "stats" | "routing" | "retries" | "terminal" | `plugin-${string}`;
+type TabId = "summary" | "definition" | "chat" | "planner-chat" | "logs" | "changes" | "review" | "pr" | "comments" | "model" | "workflow" | "documents" | "stats" | "routing" | "retries" | "terminal" | "worktree-terminal" | `plugin-${string}`;
type ActivitySegment = "current" | "feed" | "raw-logs" | "interventions";
/*
@@ -271,10 +271,13 @@ function resolveDefaultActivitySegment(initialTab: TabId | undefined): ActivityS
return initialTab === "logs" ? "feed" : "current";
}
-// Lazy-load the terminal so xterm + addons stay out of the main bundle (U11).
+// Lazy-load terminal surfaces so xterm + addons stay out of the main bundle (U11).
const LazySessionTerminal = lazy(() =>
import("./SessionTerminal").then((m) => ({ default: m.SessionTerminal })),
);
+const LazyTerminalModal = lazy(() =>
+ import("./TerminalModal").then((m) => ({ default: m.TerminalModal })),
+);
/** CLI session record fields the terminal tab needs (mirrors @fusion/core CliSession). */
export interface CliSessionSummaryRecord {
@@ -1153,6 +1156,12 @@ export function TaskDetailContent({
[cliSession, cliOneShot, cliGenericIdle],
);
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.
+ */
+ const taskWorktreeCwd = typeof task.worktree === "string" && task.worktree.trim().length > 0 ? task.worktree : undefined;
+ const showWorktreeTerminalTab = Boolean(taskWorktreeCwd) && !isWorkspaceTask(workingTask);
const cliPosture: SessionTerminalPosture | undefined = useMemo(() => {
if (!cliSession) return undefined;
const p = cliSession.autonomyPosture ?? {};
@@ -1187,6 +1196,11 @@ 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(() => {
mountedRef.current = true;
@@ -4439,7 +4453,15 @@ export function TaskDetailContent({
className={`detail-tab${activeTab === "terminal" ? " detail-tab-active" : ""}`}
onClick={() => setActiveTab("terminal")}
>
- {t("taskDetail.tabs.terminal", "Terminal")}
+ {t("taskDetail.tabs.terminal", "Session")}
+
+ )}
+ {showWorktreeTerminalTab && (
+
)}
{/* Plugin tabs */}
@@ -4824,6 +4846,19 @@ export function TaskDetailContent({
) : null}
+ ) : activeTab === "worktree-terminal" && showWorktreeTerminalTab && taskWorktreeCwd ? (
+
+
}>
+ setActiveTab("definition")}
+ embedded
+ defaultCwd={taskWorktreeCwd}
+ scopeId={task.id}
+ projectId={projectId}
+ />
+
+
) : (
<>
{/* FNXC:TaskDetailSummaryTab 2026-06-27-00:00: The former inline Definition-tab completion summary is intentionally removed to avoid duplicating the new done-only Summary tab; Definition keeps merge/retry/source metadata below. */}
diff --git a/packages/dashboard/app/components/TerminalModal.css b/packages/dashboard/app/components/TerminalModal.css
index 00b07af170..4eaf603804 100644
--- a/packages/dashboard/app/components/TerminalModal.css
+++ b/packages/dashboard/app/components/TerminalModal.css
@@ -1824,3 +1824,54 @@ reapply the mobile-only `.terminal-shortcuts--header, .terminal-connection-statu
touch-action: pan-x pan-y;
}
}
+
+/*
+FNXC:Terminal 2026-07-10-00:00:
+FN-7813 embeds the shared terminal inside Task Detail. The host must fill the parent tab panel, keep scroll/resize inside the terminal surface, and avoid overlay/floating dimensions so Task Detail owns layout at every breakpoint.
+*/
+.terminal-embedded-host {
+ display: flex;
+ flex: 1 1 auto;
+ min-height: 0;
+ width: 100%;
+ overflow: hidden;
+ background: var(--surface);
+ border: thin solid var(--border);
+ border-radius: var(--radius-lg);
+}
+
+.modal.terminal-modal.terminal-modal--embedded {
+ position: relative;
+ flex: 1 1 auto;
+ width: 100%;
+ min-width: 0;
+ min-height: 0;
+ max-width: none;
+ max-height: none;
+ height: auto;
+ border: 0;
+ border-radius: 0;
+ box-shadow: none;
+ resize: none;
+}
+
+.terminal-modal--embedded .terminal-header {
+ flex-wrap: wrap;
+ gap: var(--space-sm);
+}
+
+@media (max-width: 768px) {
+ .terminal-embedded-host {
+ border-radius: var(--radius-md);
+ }
+
+ .terminal-modal--embedded .terminal-header {
+ align-items: stretch;
+ }
+
+ .terminal-modal--embedded .terminal-tabs,
+ .terminal-modal--embedded .terminal-workspace-picker,
+ .terminal-modal--embedded .terminal-actions {
+ min-width: 0;
+ }
+}
diff --git a/packages/dashboard/app/components/TerminalModal.tsx b/packages/dashboard/app/components/TerminalModal.tsx
index dd2b73dc9c..b205a78c1b 100644
--- a/packages/dashboard/app/components/TerminalModal.tsx
+++ b/packages/dashboard/app/components/TerminalModal.tsx
@@ -516,6 +516,12 @@ interface TerminalModalProps {
initialCommand?: string;
initialCommandGeneration?: number;
projectId?: string;
+ /** Render the terminal inline inside a parent-owned layout instead of a portaled modal. */
+ embedded?: boolean;
+ /** Worktree/project directory used by the initial scoped tab. */
+ defaultCwd?: string;
+ /** Optional terminal-session namespace, usually the owning task id. */
+ scopeId?: string;
}
/**
@@ -533,7 +539,7 @@ interface TerminalModalProps {
*
* The terminal spawns a real shell (bash/zsh/powershell based on platform).
*/
-export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandGeneration = 0, projectId }: TerminalModalProps) {
+export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandGeneration = 0, projectId, embedded = false, defaultCwd, scopeId }: TerminalModalProps) {
const { t } = useTranslation("app");
const [error, setError] = useState(null);
const [exitCode, setExitCode] = useState(null);
@@ -568,9 +574,13 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
const [isMobileTerminal, setIsMobileTerminal] = useState(() => isTerminalMobileViewport());
// FNXC:TerminalFooter 2026-07-08-15:00: FN-7684 tablet tier (769-1024px, non-mobile) — keeps the desktop display modes (docked/floating/pinned-below), only the action-control render location changes.
const [isTabletTerminal, setIsTabletTerminal] = useState(() => isTerminalTabletViewport());
- const isDockedMode = !isMobileTerminal && displayMode === "docked";
- const isFloatingMode = !isMobileTerminal && displayMode === "floating";
- const isBelowMode = !isMobileTerminal && displayMode === "below";
+ /*
+ FNXC:Terminal 2026-07-10-00:00:
+ FN-7813 embedded mode is parent-layout owned: render in-flow, skip portal/overlay/display-mode chrome, and keep the shared xterm/session/resize observers so Task Detail gets the same terminal behavior without taking over the viewport.
+ */
+ const isDockedMode = !embedded && !isMobileTerminal && displayMode === "docked";
+ const isFloatingMode = !embedded && !isMobileTerminal && displayMode === "floating";
+ const isBelowMode = !embedded && !isMobileTerminal && displayMode === "below";
// FNXC:FloatingWindow 2026-06-22-21:30: The FLOATING terminal shares the SINGLE cross-type floating z-index stack (floatingWindowStack) so tapping it raises it above every other floating modal regardless of type. A fresh z is claimed each time the modal opens (see effect below); tapping the panel (pointerdown/focus capture) re-raises it. Docked/mobile modes ignore this z-index (full-width bottom panel / full-screen sheet).
const [floatingZ, setFloatingZ] = useState(() => nextFloatingZ());
const bringFloatingToFront = useCallback(() => {
@@ -1060,7 +1070,10 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
restartActiveTab,
retryBootstrap,
replaceActiveTabSession,
- } = useTerminalSessions(projectId);
+ } = useTerminalSessions(projectId, {
+ storageScope: scopeId ? `task:${scopeId}` : undefined,
+ defaultCwd,
+ });
/*
FNXC:Terminal 2026-07-06-09:15:
@@ -2293,7 +2306,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
const isLoading = !isReady || (!activeTab && !bootstrapError);
// FNXC:Terminal 2026-06-23-04:30: Always carry the base `terminal-modal-overlay` class so the no-dim/no-blur rule applies in EVERY mode (docked, floating, AND the mobile/default sheet that is neither) — the terminal must never dim the page behind it.
const overlayClassName = `modal-overlay open terminal-modal-overlay${isDockedMode ? " terminal-modal-overlay--docked" : ""}${isFloatingMode ? " terminal-modal-overlay--floating" : ""}`;
- const modalClassName = `modal terminal-modal${isMobileTerminal ? " terminal-modal--mobile" : ""}${isDockedMode ? " terminal-modal--docked" : ""}${isFloatingMode ? " terminal-modal--floating" : ""}${isBelowMode ? " terminal-modal--below" : ""}`;
+ const modalClassName = `modal terminal-modal${isMobileTerminal && !embedded ? " terminal-modal--mobile" : ""}${isDockedMode ? " terminal-modal--docked" : ""}${isFloatingMode ? " terminal-modal--floating" : ""}${isBelowMode ? " terminal-modal--below" : ""}${embedded ? " terminal-modal--embedded" : ""}`;
const modalStyle = {
...(keyboardOverlap > 0
? {
@@ -2385,7 +2398,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
{exitCode !== null && {t("terminal.exitLabel", "Exit: {{code}}", { code: exitCode })}}
{t("terminal.helpText", "Ctrl++/- zoom • ⌨ Shortcuts panel • Esc close")}
- {!isMobileTerminal && (
+ {!embedded && !isMobileTerminal && (
: }
)}
- {!isMobileTerminal && (
+ {!embedded && !isMobileTerminal && (