diff --git a/.changeset/fn-7502-terminal-below-layout.md b/.changeset/fn-7502-terminal-below-layout.md
new file mode 100644
index 0000000000..6c7aa6e95f
--- /dev/null
+++ b/.changeset/fn-7502-terminal-below-layout.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": minor
+---
+
+summary: Add a pinned below-application layout option for the dashboard terminal.
+category: feature
+dev: Terminal display mode now supports persisted docked, floating, and below layouts, with header controls replacing the footer shell.
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index c78808f8d3..e9e3fd5335 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -527,14 +527,16 @@ On Windows, the embedded terminal starts a supported shell inside Fusion, such a
Use the terminal on desktop/tablet:
1. Select the **Terminal** button in the footer executor status bar.
- Expected outcome: the terminal opens as a bottom-docked panel with the active shell session and a draggable top resize handle.
-2. Drag the top edge of the docked panel.
- Expected outcome: the panel height changes within its viewport-safe bounds and persists per project.
-3. Select **Pop out** from the terminal header.
+ Expected outcome: the terminal opens as a bottom-docked overlay panel with the active shell session, header controls for font size / clear / shortcuts / preferences, and a draggable top resize handle.
+2. Select **Pin terminal (push content)** from the terminal header.
+ Expected outcome: the terminal moves into a persisted below-application panel that reserves space instead of covering the board, chat, or right sidebar. Select **Unpin terminal (overlay content)** to return to the overlay docked panel.
+3. Drag the top edge of the docked or pinned panel.
+ Expected outcome: the panel height changes within its viewport-safe bounds and persists per project, with pinned mode clamped shorter so the application remains usable.
+4. Select **Pop out** from the terminal header.
Expected outcome: the terminal switches to a floating window that can be dragged and freely resized; size, position, and display mode are saved per project.
-4. Select **Dock** in the floating terminal.
- Expected outcome: the terminal returns to the bottom docked panel using the saved docked height.
-5. Select the scripts chevron beside the footer **Terminal** button.
+5. Select **Dock** in the floating terminal.
+ Expected outcome: the terminal returns to the bottom docked overlay panel using the saved docked height.
+6. Select the scripts chevron beside the footer **Terminal** button.
Expected outcome: the quick scripts menu opens without toggling the terminal; choosing a script runs it in the terminal, and the menu footer opens script management.
Use the terminal on mobile:
diff --git a/packages/dashboard/app/App.tsx b/packages/dashboard/app/App.tsx
index 67f40a1e10..1c5f81388f 100644
--- a/packages/dashboard/app/App.tsx
+++ b/packages/dashboard/app/App.tsx
@@ -12,6 +12,7 @@ import { AppModals } from "./components/AppModals";
import { DashboardLoader, type DashboardLoaderStage } from "./components/DashboardLoader";
import { TopProgressBar } from "./components/TopProgressBar";
import { ExecutorStatusBar } from "./components/ExecutorStatusBar";
+import { TerminalModal } from "./components/TerminalModal";
import { type CliActionId } from "./components/SessionNotificationBanner";
import {
isOnboardingCompleted,
@@ -937,15 +938,19 @@ function AppInner() {
pushNav({ type: "modal", close: modalManager.closeGitHubImport });
}, [modalManager, pushNav]);
+ const closeTerminalWithNav = useCallback(() => {
+ removeNav(modalManager.closeTerminal);
+ modalManager.closeTerminal();
+ }, [modalManager, removeNav]);
+
const toggleTerminalWithNav = useCallback(() => {
if (!modalManager.terminalOpen) {
modalManager.toggleTerminal();
pushNav({ type: "modal", close: modalManager.closeTerminal });
} else {
- removeNav(modalManager.closeTerminal);
- modalManager.toggleTerminal();
+ closeTerminalWithNav();
}
- }, [modalManager, pushNav, removeNav]);
+ }, [closeTerminalWithNav, modalManager, pushNav]);
const openFilesWithNav = useCallback((workspace?: string, initialFile?: string | null) => {
modalManager.openFiles(workspace, initialFile);
@@ -1475,6 +1480,7 @@ function AppInner() {
}
/>
+
{sidebarActive && (
{rightDock.dock}
+ {currentProject && (
+
+ )}
+
{rightDock.modal}
{executorFooterVisible && currentProject && (
{
- removeNav(modalManager.closeTerminal);
- modalManager.closeTerminal();
- }, [modalManager.closeTerminal, removeNav]);
-
const closeScriptsWithNav = useCallback(() => {
removeNav(modalManager.closeScripts);
modalManager.closeScripts();
@@ -408,14 +402,6 @@ export function AppModals({
/>
-
-
isTerminalMobileViewport());
const isDockedMode = !isMobileTerminal && displayMode === "docked";
const isFloatingMode = !isMobileTerminal && displayMode === "floating";
+ const isBelowMode = !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(() => {
@@ -604,6 +628,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
const setDisplayMode = useCallback((mode: TerminalDisplayMode) => {
setDisplayModeState(writeTerminalDisplayMode(mode, projectId));
+ window.dispatchEvent(new CustomEvent("fusion:terminal-display-mode-change", { detail: { projectId, mode } }));
}, [projectId]);
const persistFloatingSize = useCallback((size: TerminalFloatSize) => {
@@ -626,7 +651,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
Docked top-edge resize, smooth on touch + desktop (same technique as the right-dock pop-out RightDockExpandModal). On pointerdown we setPointerCapture on the handle and attach pointermove/up/cancel to the CAPTURED element (`captureTarget` = event.currentTarget), NOT `document` — capture redirects the full pointer stream for this pointerId to that element so element-scoped listeners receive every move even when the finger drifts off the handle, and they pair cleanly with the handle's `touch-action: none` (CSS) without a non-passive document listener. Moves are filtered by pointerId and coalesced into one rAF, so we set height at most once per frame and never thrash layout on a flood of touch-move events. localStorage is written only on pointerup (existing behavior). Teardown (pointerup/cancel + unmount via dragTeardownRef) cancels the pending rAF, releases pointer capture, and detaches listeners.
*/
const handleDockedResizePointerDown = useCallback((event: ReactPointerEvent) => {
- if (!isDockedMode) return;
+ if (!isDockedMode && !isBelowMode) return;
event.preventDefault();
const captureTarget = event.currentTarget;
const pointerId = event.pointerId;
@@ -641,7 +666,8 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
const handlePointerMove = (moveEvent: PointerEvent) => {
if (moveEvent.pointerId !== pointerId) return;
- latestHeight = clampTerminalDockedHeight(startHeight + (startY - moveEvent.clientY));
+ const nextHeight = isBelowMode ? startHeight + (moveEvent.clientY - startY) : startHeight + (startY - moveEvent.clientY);
+ latestHeight = isBelowMode ? clampTerminalBelowHeight(nextHeight) : clampTerminalDockedHeight(nextHeight);
if (frame) return;
frame = requestAnimationFrame(() => {
frame = 0;
@@ -656,7 +682,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
};
function handlePointerUp() {
if (frame) cancelAnimationFrame(frame);
- setDockedHeight(writeTerminalDockedHeight(latestHeight, projectId));
+ setDockedHeight(writeTerminalDockedHeight(latestHeight, projectId, isBelowMode ? "below" : "docked"));
document.body.style.userSelect = previousUserSelect;
detachListeners();
dragTeardownRef.current = null;
@@ -673,7 +699,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
captureTarget.addEventListener("pointermove", handlePointerMove);
captureTarget.addEventListener("pointerup", handlePointerUp);
captureTarget.addEventListener("pointercancel", handlePointerUp);
- }, [dockedHeight, isDockedMode, projectId]);
+ }, [dockedHeight, isBelowMode, isDockedMode, projectId]);
/*
FNXC:Terminal 2026-06-22-19:50:
@@ -2007,6 +2033,13 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
setDisplayMode(displayMode === "floating" ? "docked" : "floating");
}, [displayMode, setDisplayMode]);
+ const handleToggleBelowMode = useCallback(() => {
+ setDisplayMode(displayMode === "below" ? "docked" : "below");
+ if (displayMode !== "below") {
+ setDockedHeight((current) => clampTerminalBelowHeight(current || TERMINAL_BELOW_DEFAULT_HEIGHT));
+ }
+ }, [displayMode, setDisplayMode]);
+
const handlePreferenceFontSizeChange = useCallback(
(value: string) => {
const parsed = Number.parseInt(value, 10);
@@ -2112,7 +2145,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" : ""}`;
+ const modalClassName = `modal terminal-modal${isMobileTerminal ? " terminal-modal--mobile" : ""}${isDockedMode ? " terminal-modal--docked" : ""}${isFloatingMode ? " terminal-modal--floating" : ""}${isBelowMode ? " terminal-modal--below" : ""}`;
const modalStyle = {
...(keyboardOverlap > 0
? {
@@ -2126,6 +2159,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
}
: {}),
...(isDockedMode ? { "--terminal-docked-height": `${dockedHeight}px` } : {}),
+ ...(isBelowMode ? { "--terminal-below-height": `${clampTerminalBelowHeight(dockedHeight || TERMINAL_BELOW_DEFAULT_HEIGHT)}px` } : {}),
...(isFloatingMode
? {
"--terminal-float-x": `${floatingPosition.x}px`,
@@ -2138,36 +2172,24 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
: {}),
} as CSSProperties;
- // FNXC:FloatingWindow 2026-06-22-22:30: Portaled to document.body so the terminal shares the ONE root stacking context with the other floating modals; the shared cross-type z stack only orders correctly when all panels live at the document root. Docked/floating/mobile are all position:fixed, so portaling does not change their placement.
- return createPortal(
+ const terminalPanel = (
- {isDockedMode && (
+ {(isDockedMode || isBelowMode) && (
)}
@@ -2415,6 +2437,51 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
)}
{/*
+ FNXC:TerminalHeader 2026-07-04-19:16:
+ Footer controls live in the terminal header so docked, floating, pinned-below, and mobile layouts keep terminal actions reachable without spending a separate footer row. The pin button mirrors the right sidebar contract: aria-pressed means the persistent below-application push layout is active.
+ */}
+
+
+ {fontSize}{TERMINAL_KEY_LABELS.pxUnit}
+
+
+
+
+
+
+ {connectionStatus === "connected" && t("terminal.statusConnected", "Connected")}
+ {connectionStatus === "connecting" && t("terminal.statusConnecting", "Connecting...")}
+ {connectionStatus === "reconnecting" && t("terminal.statusReconnecting", "Reconnecting...")}
+ {connectionStatus === "disconnected" && t("terminal.statusDisconnected", "Disconnected")}
+
+ {exitCode !== null && {t("terminal.exitLabel", "Exit: {{code}}", { code: exitCode })}}
+ {t("terminal.helpText", "Ctrl++/- zoom • ⌨ Shortcuts panel • Esc close")}
+ {!isMobileTerminal && (
+
+ )}
+ {/*
FNXC:Terminal 2026-06-23-00:15:
Clear / Shortcuts / Preferences moved OUT of the header actions and DOWN into the bottom status bar (footer) next to the text-size control, so the header keeps only contextual reconnect/restart, the icon-only pop-out toggle, and close.
The pop-out/dock toggle is now ICON-ONLY (no visible "Pop out"/"Dock" text); the icon flips and the title/aria-label still announce the toggle target for accessibility.
@@ -2709,82 +2776,33 @@ export function TerminalModal({ isOpen, onClose, initialCommand, initialCommandG
)}
- {/*
- FNXC:Terminal 2026-06-23-00:15:
- Footer is laid out left-to-right as a flex row: the text-size control sits at the LEFT, followed by the relocated Clear / Shortcuts / Preferences action buttons (a grouped cluster). The connection-status text and zoom-hint copy stay on the right and collapse first on narrow widths. The whole control cluster wraps/scrolls when the footer is too narrow so docked/floating/mobile layouts never clip the buttons.
- */}
-
+ );
+ }
+
+ // FNXC:FloatingWindow 2026-06-22-22:30: Portaled to document.body so overlay terminal modes share the ONE root stacking context with other floating modals. Below mode intentionally skips the portal so it can reserve in-flow application space.
+ return createPortal(
+