fix: improve mobile terminal alternate-screen detection and scrolling
- Revert touch-action: none on .xterm-viewport so xterm.js can fall back to its own viewport scrolling when the custom handler doesn't take over. - Detect alternate screen via reference equality (term.buffer.active === term.buffer.alternate) instead of the string, which could report normal buffer incorrectly. - Lower vertical-scroll activation threshold from 4px to 2px and only prevent default once a vertical gesture is recognized. - In normal buffer use term.scrollLines() so xterm.js handles the buffer scroll consistently; in alternate screen continue sending SGR 1006 mouse-wheel sequences to tmux/vim. Quality gates: typecheck, lint clean, npm test -- --run 87 passed.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components/features
|
||||
|
||||
## role
|
||||
Houses reusable React components that implement specific product features and business logic for the web application.
|
||||
Contains reusable React components that implement specific user-facing features and functionality across the web application.
|
||||
## parent
|
||||
index: apps/web/src/components/.pi-map.index.md
|
||||
map: apps/web/src/components/.pi-map.md
|
||||
|
||||
@@ -4,10 +4,10 @@ dir: apps/web/src/components/features
|
||||
index: apps/web/src/components/features/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Houses reusable React components that implement specific product features and business logic for the web application.
|
||||
Contains reusable React components that implement specific user-facing features and functionality across the web application.
|
||||
## files
|
||||
## arch
|
||||
Feature-based component organization with co-located UI pieces, likely composed of smaller design-system elements and connected to domain hooks/stores for stateful functionality.
|
||||
Feature-based component organization with domain-specific UI building blocks following React composition patterns, likely co-located with related hooks, utilities, or sub-components for each feature area.
|
||||
## tags
|
||||
-
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components/features/terminal
|
||||
|
||||
## role
|
||||
Provides cross-platform terminal UI components with session management, special key input, and WebSocket-backed xterm.js integration for web-based terminal emulation.
|
||||
Provides a cross-platform terminal emulator interface with WebSocket backend connectivity, supporting multiple sessions, special key input, and responsive desktop/mobile layouts.
|
||||
## parent
|
||||
index: apps/web/src/components/features/.pi-map.index.md
|
||||
map: apps/web/src/components/features/.pi-map.md
|
||||
|
||||
@@ -4,7 +4,7 @@ dir: apps/web/src/components/features/terminal
|
||||
index: apps/web/src/components/features/terminal/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides cross-platform terminal UI components with session management, special key input, and WebSocket-backed xterm.js integration for web-based terminal emulation.
|
||||
Provides a cross-platform terminal emulator interface with WebSocket backend connectivity, supporting multiple sessions, special key input, and responsive desktop/mobile layouts.
|
||||
## files
|
||||
- DesktopTerminalView.tsx | Renders a desktop-optimized terminal view with session tabs, fullscreen mode, font controls, and reset confirmation dialog | exp: DesktopTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../../api/terminal, React, TerminalComponent, TerminalSessionTabs, TerminalSession type
|
||||
- MobileTerminalView.tsx | Renders a mobile-optimized terminal interface with toolbar, session tabs, terminal output, and special keys input controls. | exp: MobileTerminalView | dep: react, ./terminal, ./terminal-session-tabs, ../../icon, ./special-keys-strip, ./special-keys-panel, ../../../hooks/use-special-keys, ../../../api/terminal
|
||||
@@ -12,9 +12,9 @@ Provides cross-platform terminal UI components with session management, special
|
||||
- special-keys-strip.tsx | Renders a strip of buttons for sending special keyboard keys (Esc, Tab, arrows, etc.) with optional modifier support for a terminal interface. | exp: SpecialKeysStrip | dep: react, ../../../hooks/use-special-keys, React, use-special-keys hook
|
||||
- terminal-session-tabs.test.tsx | Tests a React component that renders interactive terminal session tabs with selection, close confirmation, renaming, creation limits, and status indicators. | dep: @testing-library/react, vitest, ./terminal-session-tabs
|
||||
- terminal-session-tabs.tsx | Renders a tabbed interface for managing multiple terminal sessions with selection, creation, renaming, and close confirmation features. | exp: TerminalSessionInfo, TerminalSessionTabsProps, TerminalSessionTabs | dep: react, React
|
||||
- terminal.tsx | A React component that renders an interactive xterm.js terminal connected to a WebSocket backend, with mobile touch support, flow control, heartbeat monitoring, and automatic reconnection. | exp: TerminalProps, TerminalRef, TerminalComponent | dep: react, xterm, xterm-addon-fit, xterm-addon-web-links, xterm/css/xterm.css, ../../../hooks/use-special-keys
|
||||
- terminal.tsx | A React component that renders an interactive terminal emulator using xterm.js, connecting to a WebSocket backend with features like mobile touch scrolling, font resizing, flow control, heartbeat monitoring, and automatic reconnection. | exp: TerminalProps, TerminalRef, TerminalComponent | dep: react, xterm, xterm-addon-fit, xterm-addon-web-links, xterm/css/xterm.css, ../../../hooks/use-special-keys
|
||||
## arch
|
||||
Feature-based component architecture with platform-specific view splitting (desktop/mobile), compound component pattern for terminal sessions, and adapter pattern for special key input variants (panel/strip).
|
||||
Component-based architecture with platform-specific view variants (Desktop/Mobile), compound component pattern for terminal subsystems (tabs, special keys, xterm.js integration), and WebSocket-based real-time communication with reconnection and heartbeat monitoring.
|
||||
## tags
|
||||
terminal, special, keys, session, react, tabs, view, renders
|
||||
## symbols
|
||||
|
||||
@@ -355,10 +355,9 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
// In normal mode xterm.js has a scrollable viewport; in alternate
|
||||
// screen (tmux/vim) there is no scrollback and the only way to
|
||||
// scroll is to send mouse-wheel protocol sequences to the
|
||||
// application. We detect which situation we're in by checking
|
||||
// the active buffer type. We also lock the browser into the
|
||||
// terminal area: touchstart prevents the browser from starting a
|
||||
// page-scroll gesture, so swipes always go to the terminal.
|
||||
// application. We detect the active buffer by reference equality
|
||||
// (term.buffer.active === term.buffer.alternate) because the
|
||||
// `type` string can be unreliable in some xterm.js versions.
|
||||
let touchCleanup: (() => void) | undefined;
|
||||
if (isMobile) {
|
||||
let startY = 0;
|
||||
@@ -388,8 +387,10 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
scrollPending = 0;
|
||||
};
|
||||
|
||||
const isNormalBuffer = () => {
|
||||
return termRef.current?.buffer.active.type === "normal";
|
||||
const isAlternateScreen = () => {
|
||||
const term = termRef.current;
|
||||
if (!term) return false;
|
||||
return term.buffer.active === term.buffer.alternate;
|
||||
};
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
@@ -405,35 +406,36 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
const touch = e.touches[0];
|
||||
const deltaY = startY - touch.clientY;
|
||||
const deltaX = Math.abs(startX - touch.clientX);
|
||||
|
||||
// Decide early whether this is a vertical scroll gesture.
|
||||
if (!isScrolling) {
|
||||
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 4) {
|
||||
if (Math.abs(deltaY) > deltaX && Math.abs(deltaY) > 2) {
|
||||
isScrolling = true;
|
||||
}
|
||||
}
|
||||
if (isScrolling) {
|
||||
// Always stop the browser from treating this as a page scroll.
|
||||
e.preventDefault();
|
||||
if (!isScrolling) return;
|
||||
|
||||
const normalBuffer = isNormalBuffer();
|
||||
const viewport = container.querySelector(
|
||||
".xterm-viewport",
|
||||
) as HTMLElement | null;
|
||||
// Take over the gesture so the page/toolbar never scrolls.
|
||||
e.preventDefault();
|
||||
|
||||
if (normalBuffer && viewport) {
|
||||
// Normal buffer: scroll the xterm viewport directly.
|
||||
viewport.scrollTop += deltaY;
|
||||
} else {
|
||||
// Alternate screen (tmux/vim): accumulate the swipe and
|
||||
// send SGR 1006 mouse-wheel events in steps.
|
||||
scrollPending += deltaY;
|
||||
flushScroll();
|
||||
if (isAlternateScreen()) {
|
||||
// Alternate screen (tmux/vim): accumulate the swipe and
|
||||
// send SGR 1006 mouse-wheel events in steps.
|
||||
scrollPending += deltaY;
|
||||
flushScroll();
|
||||
} else if (termRef.current) {
|
||||
// Normal buffer: let xterm.js scroll its own viewport by
|
||||
// the number of lines corresponding to the swipe distance.
|
||||
const lineHeight =
|
||||
termRef.current.options.fontSize != null
|
||||
? (termRef.current.options.fontSize as number) * 1.2
|
||||
: 10;
|
||||
const lines = Math.round(deltaY / lineHeight);
|
||||
if (lines !== 0) {
|
||||
termRef.current.scrollLines(lines);
|
||||
}
|
||||
startY = touch.clientY;
|
||||
} else if (Math.abs(deltaY) > 4 || Math.abs(deltaX) > 4) {
|
||||
// Once the user has moved far enough to be considered a
|
||||
// gesture, prevent any default page scroll/pinch behavior.
|
||||
e.preventDefault();
|
||||
}
|
||||
startY = touch.clientY;
|
||||
};
|
||||
const onTouchEnd = () => {
|
||||
if (isScrolling) {
|
||||
|
||||
Reference in New Issue
Block a user