diff --git a/.changeset/FN-7417-windows-terminal-popup.md b/.changeset/FN-7417-windows-terminal-popup.md
new file mode 100644
index 0000000000..3b9a7fea24
--- /dev/null
+++ b/.changeset/FN-7417-windows-terminal-popup.md
@@ -0,0 +1,7 @@
+---
+"@runfusion/fusion": patch
+---
+
+summary: Stop recurring Windows Terminal warning popups during terminal startup.
+category: fix
+dev: Keeps embedded terminal bootstrap on supported shells and surfaces actionable inline errors.
diff --git a/docs/dashboard-guide.md b/docs/dashboard-guide.md
index c14c313021..32a7db3446 100644
--- a/docs/dashboard-guide.md
+++ b/docs/dashboard-guide.md
@@ -508,6 +508,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.
+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:
1. Select the **Terminal** button in the footer executor status bar.
diff --git a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx
index 4172628a3d..c192558eac 100644
--- a/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx
+++ b/packages/dashboard/app/components/__tests__/TerminalModal.test.tsx
@@ -664,6 +664,37 @@ describe("TerminalModal", () => {
expect(refreshBtn.textContent).toContain("Refresh page");
});
+ it("shows Windows Terminal startup guidance inline on desktop and mobile without native dialog hooks", async () => {
+ const mockRetryBootstrap = vi.fn();
+ const windowsTerminalMessage =
+ "Fusion could not start an embedded terminal shell on Windows. Use Command Prompt or PowerShell for the embedded terminal, or install/repair Windows Terminal separately with `winget install Microsoft.WindowsTerminal` if you want Windows Terminal outside Fusion.";
+ mockUseTerminalSessions.mockReturnValue({
+ ...defaultSessionState,
+ tabs: [],
+ activeTab: null,
+ bootstrapError: windowsTerminalMessage,
+ retryBootstrap: mockRetryBootstrap,
+ });
+
+ const { rerender } = render();
+
+ expect(screen.getByTestId("terminal-bootstrap-error")).toHaveTextContent("Command Prompt or PowerShell");
+ expect(screen.getByTestId("terminal-bootstrap-error")).toHaveTextContent("winget install Microsoft.WindowsTerminal");
+ expect(screen.getByTestId("terminal-bootstrap-error")).not.toHaveTextContent("1.24.11321.0");
+ expect(screen.getByTestId("terminal-retry-btn")).toBeTruthy();
+
+ const previousInnerWidth = window.innerWidth;
+ Object.defineProperty(window, "innerWidth", { value: 390, configurable: true });
+ try {
+ rerender();
+ expect(screen.getByTestId("terminal-bootstrap-error")).toHaveTextContent("Command Prompt or PowerShell");
+ fireEvent.click(screen.getByTestId("terminal-retry-btn"));
+ expect(mockRetryBootstrap).toHaveBeenCalledTimes(1);
+ } finally {
+ Object.defineProperty(window, "innerWidth", { value: previousInnerWidth, configurable: true });
+ }
+ });
+
it("retry button calls retryBootstrap from the hook", async () => {
const mockRetryBootstrap = vi.fn();
mockUseTerminalSessions.mockReturnValue({
diff --git a/packages/dashboard/app/hooks/__tests__/useTerminalSessions.test.ts b/packages/dashboard/app/hooks/__tests__/useTerminalSessions.test.ts
index fafcedefcd..1c8b809d93 100644
--- a/packages/dashboard/app/hooks/__tests__/useTerminalSessions.test.ts
+++ b/packages/dashboard/app/hooks/__tests__/useTerminalSessions.test.ts
@@ -882,9 +882,33 @@ describe("useTerminalSessions", () => {
expect(result.current.bootstrapError).toBe("Server unreachable");
});
- // No tabs should be created
+ // No tabs should be created, and the failed generation must not auto-loop.
expect(result.current.tabs.length).toBe(0);
expect(result.current.activeTab).toBeNull();
+ expect(mockCreateTerminalSession).toHaveBeenCalledTimes(1);
+ });
+
+ it("shows the Windows Terminal version-only failure once until explicit retry", async () => {
+ localStorageMock.getItem.mockReturnValue(null);
+ mockListTerminalSessions.mockResolvedValue([]);
+ const windowsTerminalMessage =
+ "Fusion could not start an embedded terminal shell on Windows. Use Command Prompt or PowerShell for the embedded terminal, or install/repair Windows Terminal separately with `winget install Microsoft.WindowsTerminal` if you want Windows Terminal outside Fusion.";
+ mockCreateTerminalSession.mockRejectedValue(new Error(windowsTerminalMessage));
+
+ const { result, rerender } = renderHook(() => useTerminalSessions(TEST_PROJECT_ID));
+
+ await waitFor(() => {
+ expect(result.current.bootstrapError).toBe(windowsTerminalMessage);
+ });
+
+ rerender();
+ await new Promise((resolve) => setTimeout(resolve, 0));
+
+ expect(result.current.tabs).toHaveLength(0);
+ expect(result.current.activeTab).toBeNull();
+ expect(result.current.bootstrapError).toBe(windowsTerminalMessage);
+ expect(result.current.bootstrapError).not.toContain("1.24.11321.0");
+ expect(mockCreateTerminalSession).toHaveBeenCalledTimes(1);
});
it("sets bootstrapError with fallback message for non-Error throws", async () => {
diff --git a/packages/dashboard/app/hooks/useTerminalSessions.ts b/packages/dashboard/app/hooks/useTerminalSessions.ts
index c1dc6ee12b..f5565cef1c 100644
--- a/packages/dashboard/app/hooks/useTerminalSessions.ts
+++ b/packages/dashboard/app/hooks/useTerminalSessions.ts
@@ -146,6 +146,7 @@ export function useTerminalSessions(projectId?: string): UseTerminalSessionsRetu
// Ref-based generation token to protect against stale completions from prior
// bootstrap attempts. Only the current generation may mutate state.
const generationRef = useRef(0);
+ const bootstrapCreateInFlightGenerationRef = useRef(null);
useEffect(() => {
generationRef.current += 1;
@@ -241,12 +242,19 @@ export function useTerminalSessions(projectId?: string): UseTerminalSessionsRetu
// Auto-create first tab if no tabs exist after validation
useEffect(() => {
- if (tabs.length === 0 && isReady && serverAvailable) {
+ if (tabs.length === 0 && isReady && serverAvailable && !bootstrapError) {
// Capture current generation so only this attempt's result is accepted
const gen = generationRef.current;
+ if (bootstrapCreateInFlightGenerationRef.current === gen) return;
// Small delay to avoid race condition with the validation effect
const timeout = setTimeout(() => {
+ if (bootstrapCreateInFlightGenerationRef.current === gen) return;
+ bootstrapCreateInFlightGenerationRef.current = gen;
+ /*
+ FNXC:WindowsTerminalStartup 2026-07-02-07:45:
+ Terminal bootstrap failures must render once inside Fusion and then wait for an explicit Retry, so Windows Terminal help/version output cannot recur through an automatic create-session loop.
+ */
withTimeout(
createTerminalSession(undefined, undefined, undefined, projectId),
BOOTSTRAP_CREATE_TIMEOUT_MS,
@@ -284,11 +292,16 @@ export function useTerminalSessions(projectId?: string): UseTerminalSessionsRetu
const message =
err instanceof Error ? err.message : typeof err === "string" ? err : "Failed to create terminal session";
setBootstrapError(message);
+ })
+ .finally(() => {
+ if (bootstrapCreateInFlightGenerationRef.current === gen) {
+ bootstrapCreateInFlightGenerationRef.current = null;
+ }
});
}, 0);
return () => clearTimeout(timeout);
}
- }, [isReady, serverAvailable, tabs.length, retryGeneration]); // Run when ready or when tabs become empty
+ }, [bootstrapError, isReady, serverAvailable, tabs.length, retryGeneration]); // Run when ready or when tabs become empty
/**
* Internal create tab function (used for auto-creation and user-initiated creation).
@@ -491,6 +504,7 @@ export function useTerminalSessions(projectId?: string): UseTerminalSessionsRetu
const retryBootstrap = useCallback((): void => {
setBootstrapError(null);
generationRef.current += 1;
+ bootstrapCreateInFlightGenerationRef.current = null;
setRetryGeneration((g) => g + 1);
}, []);
diff --git a/packages/dashboard/src/__tests__/routes-automation.test.ts b/packages/dashboard/src/__tests__/routes-automation.test.ts
index 1dbb8dd8e7..006aa2a18b 100644
--- a/packages/dashboard/src/__tests__/routes-automation.test.ts
+++ b/packages/dashboard/src/__tests__/routes-automation.test.ts
@@ -369,7 +369,7 @@ describe("Terminal session routes", () => {
["invalid_shell", 400, "Shell not allowed. Please use a supported shell (bash, zsh, sh, cmd, powershell)."],
["invalid_cwd", 400, "Terminal working directory is not an authorized project or task worktree."],
["pty_load_failed", 503, "Terminal service unavailable. The PTY module could not be loaded."],
- ["pty_spawn_failed", 500, "Failed to start terminal shell process."],
+ ["pty_spawn_failed", 500, terminalServiceModule.WINDOWS_TERMINAL_EMBEDDED_STARTUP_ERROR],
] as const)("returns %s errors with the correct status and body", async (code, status, error) => {
const mockService = {
createSession: vi.fn().mockResolvedValue({
@@ -394,6 +394,35 @@ describe("Terminal session routes", () => {
terminalServiceSpy.mockRestore();
});
+ it("returns an actionable structured error instead of raw Windows Terminal version text", async () => {
+ const mockService = {
+ createSession: vi.fn().mockResolvedValue({
+ success: false,
+ code: "pty_spawn_failed",
+ error: terminalServiceModule.WINDOWS_TERMINAL_EMBEDDED_STARTUP_ERROR,
+ }),
+ };
+ const terminalServiceSpy = vi.spyOn(terminalServiceModule, "getTerminalService").mockReturnValue(mockService as any);
+
+ const res = await REQUEST(
+ buildApp(),
+ "POST",
+ "/api/terminal/sessions",
+ JSON.stringify({}),
+ { "Content-Type": "application/json" },
+ );
+
+ expect(res.status).toBe(500);
+ expect(res.body).toEqual({
+ error: terminalServiceModule.WINDOWS_TERMINAL_EMBEDDED_STARTUP_ERROR,
+ details: { code: "pty_spawn_failed" },
+ });
+ expect(JSON.stringify(res.body)).not.toContain("Windows Terminal\\n1.24.11321.0");
+ expect(mockService.createSession).toHaveBeenCalledTimes(1);
+
+ terminalServiceSpy.mockRestore();
+ });
+
it("returns 201 for a successful session creation", async () => {
const mockService = {
createSession: vi.fn().mockResolvedValue({
diff --git a/packages/dashboard/src/__tests__/terminal-service.test.ts b/packages/dashboard/src/__tests__/terminal-service.test.ts
index d4fbaebf6a..7b51aa792e 100644
--- a/packages/dashboard/src/__tests__/terminal-service.test.ts
+++ b/packages/dashboard/src/__tests__/terminal-service.test.ts
@@ -1,9 +1,13 @@
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
+import * as fs from "node:fs";
+import * as nodePty from "node-pty";
import {
READY_QUIET_WINDOW_MS,
READY_TIMEOUT_MS,
TerminalService,
STALE_SESSION_THRESHOLD_MS,
+ WINDOWS_TERMINAL_EMBEDDED_STARTUP_ERROR,
+ __setTerminalPlatformForTests,
} from "../terminal-service.js";
import { runGitCommand } from "../routes/resolve-diff-base.js";
@@ -53,6 +57,8 @@ vi.mock("../routes/resolve-diff-base.js", () => ({
runGitCommand: vi.fn(),
}));
+const ORIGINAL_SHELL = process.env.SHELL;
+
describe("TerminalService", () => {
let service: TerminalService;
const projectRoot = "/test/project";
@@ -60,6 +66,7 @@ describe("TerminalService", () => {
beforeEach(() => {
vi.clearAllMocks();
service = new TerminalService(projectRoot, 10);
+ vi.mocked(nodePty.spawn).mockImplementation(() => mockPtyProcess as never);
mockPtyProcess._onDataCallback = null;
mockPtyProcess._onExitCallback = null;
mockStat.mockResolvedValue({ isDirectory: () => true });
@@ -68,6 +75,13 @@ describe("TerminalService", () => {
afterEach(() => {
service.cleanup();
+ __setTerminalPlatformForTests(null);
+ vi.restoreAllMocks();
+ if (ORIGINAL_SHELL === undefined) {
+ delete process.env.SHELL;
+ } else {
+ process.env.SHELL = ORIGINAL_SHELL;
+ }
});
describe("createSession", () => {
@@ -107,6 +121,78 @@ describe("TerminalService", () => {
});
});
+ it("does not select or probe Windows Terminal when wt.exe is present on Windows", async () => {
+ __setTerminalPlatformForTests("win32");
+ vi.mocked(fs.existsSync).mockImplementation((candidate) => {
+ const value = String(candidate).toLowerCase();
+ return value === "wt.exe" || value.endsWith("\\cmd.exe") || value === "cmd.exe";
+ });
+ process.env.SHELL = "wt.exe";
+ const windowsService = new TerminalService(projectRoot, 10);
+
+ const result = await windowsService.createSession();
+
+ expect(result.success).toBe(true);
+ expect(nodePty.spawn).toHaveBeenCalledWith(
+ "C:\\Windows\\System32\\cmd.exe",
+ [],
+ expect.objectContaining({ cwd: projectRoot }),
+ );
+ expect(nodePty.spawn).not.toHaveBeenCalledWith("wt.exe", expect.anything(), expect.anything());
+ windowsService.cleanup();
+ });
+
+ it("rejects explicit Windows Terminal shells before spawning an embedded PTY", async () => {
+ __setTerminalPlatformForTests("win32");
+ const windowsService = new TerminalService(projectRoot, 10);
+
+ const result = await windowsService.createSession({ shell: "wt.exe" });
+
+ expect(result).toEqual({
+ success: false,
+ code: "invalid_shell",
+ error: "Shell not allowed. Please use a supported shell (bash, zsh, sh, cmd, powershell).",
+ });
+ expect(nodePty.spawn).not.toHaveBeenCalled();
+ windowsService.cleanup();
+ });
+
+ it("maps Windows Terminal version-only spawn output to an actionable inline startup error", async () => {
+ __setTerminalPlatformForTests("win32");
+ vi.mocked(fs.existsSync).mockImplementation((candidate) => String(candidate).toLowerCase().endsWith("cmd.exe"));
+ vi.mocked(nodePty.spawn).mockImplementation(() => {
+ throw new Error("Windows Terminal\n1.24.11321.0");
+ });
+ const windowsService = new TerminalService(projectRoot, 10);
+
+ const result = await windowsService.createSession();
+
+ expect(result).toEqual({
+ success: false,
+ code: "pty_spawn_failed",
+ error: WINDOWS_TERMINAL_EMBEDDED_STARTUP_ERROR,
+ });
+ expect(result.success ? result.session.shell : result.error).not.toContain("1.24.11321.0");
+ windowsService.cleanup();
+ });
+
+ it("preserves non-Windows shell detection when wt.exe is present in the environment", async () => {
+ __setTerminalPlatformForTests("linux");
+ process.env.SHELL = "/bin/bash";
+ vi.mocked(fs.existsSync).mockImplementation((candidate) => ["/bin/bash", "/bin/sh"].includes(String(candidate)));
+ const linuxService = new TerminalService(projectRoot, 10);
+
+ const result = await linuxService.createSession();
+
+ expect(result.success).toBe(true);
+ expect(nodePty.spawn).toHaveBeenCalledWith(
+ "/bin/bash",
+ ["--login"],
+ expect.objectContaining({ cwd: projectRoot }),
+ );
+ linuxService.cleanup();
+ });
+
it("allows an explicit project-root cwd", async () => {
const result = await service.createSession({ cwd: projectRoot });
diff --git a/packages/dashboard/src/terminal-service.ts b/packages/dashboard/src/terminal-service.ts
index a7e97c7c52..60aaddeb96 100644
--- a/packages/dashboard/src/terminal-service.ts
+++ b/packages/dashboard/src/terminal-service.ts
@@ -69,6 +69,19 @@ const STRIP_ENV_VARS = [
"FUSION_API_KEY",
];
+let platformOverrideForTests: NodeJS.Platform | null = null;
+
+function getTerminalPlatform(): NodeJS.Platform {
+ return platformOverrideForTests ?? os.platform();
+}
+
+export function __setTerminalPlatformForTests(platform: NodeJS.Platform | null): void {
+ platformOverrideForTests = platform;
+}
+
+export const WINDOWS_TERMINAL_EMBEDDED_STARTUP_ERROR =
+ "Fusion could not start an embedded terminal shell on Windows. Use Command Prompt or PowerShell for the embedded terminal, or install/repair Windows Terminal separately with `winget install Microsoft.WindowsTerminal` if you want Windows Terminal outside Fusion.";
+
export interface TerminalSession {
id: string;
pty: IPty;
@@ -139,8 +152,7 @@ export class TerminalService extends EventEmitter {
private sessions: Map = new Map();
private dataCallbacks: Set = new Set();
private exitCallbacks: Set = new Set();
- private isWindows = os.platform() === "win32";
- private projectRoot: string;
+ private isWindows = getTerminalPlatform() === "win32"; private projectRoot: string;
private maxSessions: number;
private registeredWorktreeCache: Map = new Map();
@@ -166,7 +178,7 @@ export class TerminalService extends EventEmitter {
* Get the default allowed shells for the current platform
*/
private getAllowedShells(): string[] {
- const platform = os.platform();
+ const platform = getTerminalPlatform();
return ALLOWED_SHELL_PATHS[platform] || ALLOWED_SHELL_PATHS.linux;
}
@@ -183,7 +195,7 @@ export class TerminalService extends EventEmitter {
* Detect the best shell for the current platform
*/
detectShell(): { shell: string; args: string[] } {
- const platform = os.platform();
+ const platform = getTerminalPlatform();
const allowedShells = this.getAllowedShells();
// Helper to get basename handling both path separators
@@ -207,7 +219,11 @@ export class TerminalService extends EventEmitter {
return ["--login"];
};
- // First try user's shell from env if it's allowed
+ /*
+ FNXC:WindowsTerminalStartup 2026-07-02-07:45:
+ Fusion's embedded terminal must not invoke or probe Windows Terminal (`wt.exe`) because it is an external terminal host, not a PTY shell; the Windows startup path stays on supported shells and reports actionable Fusion-owned errors instead of recurring native Windows Terminal help/version dialogs.
+ */
+ // First try user's shell from env if it's allowed. Windows intentionally ignores SHELL because values such as wt.exe are terminal hosts, not embedded shells.
const userShell = process.env.SHELL;
if (userShell && platform !== "win32") {
const normalizedUserShell = this.isWindows ? userShell.toLowerCase() : userShell;
@@ -243,7 +259,7 @@ export class TerminalService extends EventEmitter {
cwd: string,
): Record {
return {
- platform: os.platform(),
+ platform: getTerminalPlatform(),
projectRoot: this.projectRoot,
cwd,
requestedShell: requestedShell ?? null,
@@ -580,7 +596,9 @@ export class TerminalService extends EventEmitter {
return {
success: false,
code: "pty_spawn_failed",
- error: "Failed to start terminal shell process.",
+ error: this.isWindows
+ ? WINDOWS_TERMINAL_EMBEDDED_STARTUP_ERROR
+ : "Failed to start terminal shell process.",
};
}