feat(FN-1739): merge fusion/fn-1739

This commit is contained in:
gsxdsm
2026-04-14 10:29:21 -07:00
parent b4df0cbb34
commit 3a7c13b710
3 changed files with 93 additions and 10 deletions

View File

@@ -357,7 +357,7 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
cursorBlink: true,
cursorStyle: "block",
fontSize: 14,
fontFamily: "monospace",
fontFamily: 'ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace',
theme: {
background: "#1e1e1e",
foreground: "#d4d4d4",
@@ -384,15 +384,19 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
terminal.loadAddon(webLinksAddon);
// Try to load WebGL addon for better performance
try {
const { WebglAddon } = await import("@xterm/addon-webgl");
const webglAddon = new WebglAddon();
webglAddon.onContextLoss(() => {
webglAddon.dispose();
});
terminal.loadAddon(webglAddon);
} catch {
// WebGL not available, fallback to canvas
// Skip WebGL on mobile devices to avoid rendering artifacts (e.g., garbled
// Unicode characters in powerline prompt symbols on iOS Safari/WebKit).
if (!isMobileDevice()) {
try {
const { WebglAddon } = await import("@xterm/addon-webgl");
const webglAddon = new WebglAddon();
webglAddon.onContextLoss(() => {
webglAddon.dispose();
});
terminal.loadAddon(webglAddon);
} catch {
// WebGL not available, fallback to canvas
}
}
// Open terminal in container

View File

@@ -865,6 +865,84 @@ describe("TerminalModal", () => {
});
});
// --- FN-1739 mobile WebGL skip regression tests ---
describe("mobile WebGL skip (FN-1739)", () => {
let savedInnerWidth: typeof window.innerWidth;
let savedOntouchstart: typeof window.ontouchstart;
let savedNavigator: typeof navigator;
beforeEach(() => {
savedInnerWidth = window.innerWidth;
savedOntouchstart = window.ontouchstart;
savedNavigator = navigator;
// Mock WebGL addon to track if it's loaded
vi.mock("@xterm/addon-webgl", () => ({
WebglAddon: vi.fn(() => ({
onContextLoss: vi.fn(),
dispose: vi.fn(),
})),
}));
});
afterEach(() => {
Object.defineProperty(window, "innerWidth", {
value: savedInnerWidth,
writable: true,
configurable: true,
});
Object.defineProperty(window, "ontouchstart", {
value: savedOntouchstart,
writable: true,
configurable: true,
});
Object.defineProperty(navigator, "maxTouchPoints", {
value: (savedNavigator as any).maxTouchPoints,
writable: true,
configurable: true,
});
});
function simulateMobileDevice() {
Object.defineProperty(window, "innerWidth", {
value: 375,
writable: true,
configurable: true,
});
Object.defineProperty(window, "ontouchstart", {
value: undefined,
writable: true,
configurable: true,
});
Object.defineProperty(navigator, "maxTouchPoints", {
value: 2,
writable: true,
configurable: true,
});
}
it("does not load WebGL addon when device is mobile", async () => {
simulateMobileDevice();
// Import WebGL addon mock to get reference for assertions
const webglModule = await import("@xterm/addon-webgl");
const loadAddonSpy = vi.spyOn(mockTerminalInstance, "loadAddon");
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);
// Wait for xterm initialization to complete
await waitFor(() => {
expect(mockTerminalInstance.open).toHaveBeenCalled();
});
// WebGL addon constructor should NOT have been called
expect(webglModule.WebglAddon).not.toHaveBeenCalled();
// loadAddon should NOT have been called with WebGL addon
expect(loadAddonSpy).not.toHaveBeenCalledWith(expect.any(Object));
});
});
describe("xterm import MIME type retry", () => {
function isXtermImportBatch(values: Iterable<unknown>): values is Promise<unknown>[] {
return (