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

This commit is contained in:
Fusion
2026-04-18 20:00:58 -07:00
committed by gsxdsm
parent 898d4d70a5
commit 453076f666
6 changed files with 306 additions and 16 deletions

View File

@@ -23,18 +23,19 @@ describe("terminal helper textarea CSS contract", () => {
expect(ruleBody).toMatch(/height:\s*1px\b/);
});
it("does not disable pointer events on the helper textarea", () => {
it("anchors the helper textarea inside the terminal bounds", () => {
const ruleBody = findHelperTextareaRule();
expect(ruleBody).not.toMatch(/pointer-events\s*:\s*none\b/);
expect(ruleBody).toMatch(/top:\s*0\b/);
expect(ruleBody).toMatch(/left:\s*0\b/);
});
it("positions the helper textarea off-screen", () => {
it("prevents direct pointer interaction with the helper textarea", () => {
const ruleBody = findHelperTextareaRule();
expect(ruleBody).toMatch(/top:\s*-9999px\b/);
expect(ruleBody).toMatch(/pointer-events\s*:\s*none\b/);
});
it("keeps the helper textarea invisible", () => {
it("keeps the helper textarea effectively invisible", () => {
const ruleBody = findHelperTextareaRule();
expect(ruleBody).toMatch(/opacity:\s*0\b/);
expect(ruleBody).toMatch(/opacity:\s*0\.01\b/);
});
});

View File

@@ -679,6 +679,34 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
}
}, [connectionStatus]);
/**
* On mobile browsers, opening the soft keyboard requires focus to happen
* within a real user gesture. Programmatic focus in async effects is often
* ignored even though xterm stays connected and receives output.
*/
const handleTerminalGestureFocus = useCallback(() => {
if (!terminalRef.current) return;
// Ensure xterm updates its own focus state first.
xtermRef.current?.focus();
const helperTextarea = terminalRef.current.querySelector(
".xterm-helper-textarea",
) as HTMLTextAreaElement | undefined;
if (!helperTextarea) return;
try {
helperTextarea.focus({ preventScroll: true });
} catch {
helperTextarea.focus();
}
// Keep caret at end so subsequent key presses append naturally.
const caretPos = helperTextarea.value.length;
helperTextarea.setSelectionRange(caretPos, caretPos);
}, []);
/**
* Auto-recover when the server reports the session is invalid (code 4004).
*
@@ -953,6 +981,8 @@ export function TerminalModal({ isOpen, onClose, initialCommand, projectId }: Te
ref={terminalRef}
className="terminal-xterm"
data-testid="terminal-xterm"
onPointerDown={handleTerminalGestureFocus}
onTouchStart={handleTerminalGestureFocus}
/>
</div>

View File

@@ -3602,6 +3602,45 @@ describe("TerminalModal — xterm focus initialization (FN-1602)", () => {
expect(screen.getByTestId("terminal-modal")).toBeTruthy();
});
});
it("focuses xterm helper textarea on user pointer gesture", async () => {
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);
await waitFor(() => {
expect(mockTerminalInstance.open).toHaveBeenCalled();
});
const terminalDiv = screen.getByTestId("terminal-xterm");
const helperTextarea = document.createElement("textarea");
helperTextarea.className = "xterm-helper-textarea";
const focusSpy = vi.spyOn(helperTextarea, "focus");
const setSelectionRangeSpy = vi.spyOn(helperTextarea, "setSelectionRange");
terminalDiv.appendChild(helperTextarea);
fireEvent.pointerDown(terminalDiv);
expect(mockTerminalInstance.focus).toHaveBeenCalled();
expect(focusSpy).toHaveBeenCalled();
expect(setSelectionRangeSpy).toHaveBeenCalledWith(0, 0);
});
it("focuses xterm helper textarea on touch gesture", async () => {
render(<TerminalModal isOpen={true} onClose={mockOnClose} />);
await waitFor(() => {
expect(mockTerminalInstance.open).toHaveBeenCalled();
});
const terminalDiv = screen.getByTestId("terminal-xterm");
const helperTextarea = document.createElement("textarea");
helperTextarea.className = "xterm-helper-textarea";
const focusSpy = vi.spyOn(helperTextarea, "focus");
terminalDiv.appendChild(helperTextarea);
fireEvent.touchStart(terminalDiv);
expect(focusSpy).toHaveBeenCalled();
});
});
// --- FN-1765: Project-context propagation ---

View File

@@ -9908,12 +9908,10 @@ body {
}
/*
* Override xterm's hidden textarea positioning so mobile browsers show the
* virtual keyboard when the terminal is tapped. The upstream CSS sets
* width:0, height:0 and left:-9999em which causes many mobile browsers to
* skip showing the soft keyboard entirely. We give the textarea minimal
* (1×1 px) dimensions and move it above the viewport instead of to the
* left — this keeps it invisible while remaining a valid focus target.
* Keep xterm's helper textarea tiny and hidden off-screen.
*
* The textarea is still focusable via pointer/touch handlers in TerminalModal,
* which is required for mobile keyboard activation.
*/
.terminal-xterm .xterm .xterm-helper-textarea {
left: 0 !important;
@@ -9921,7 +9919,7 @@ body {
width: 1px !important;
height: 1px !important;
opacity: 0 !important;
z-index: -1 !important;
z-index: 1 !important;
}
.terminal-loading {