fix: route mobile terminal swipes to the terminal, not the page layout
- Add touch-action: none and overscroll-behavior: none to mobile terminal
page, content, wrapper, container, and xterm viewport so the browser
never treats swipes as page/address-bar scrolling or pull-to-refresh.
- Make .terminal-page.mobile fixed-position to prevent viewport layout
scroll; keep the overlay toolbar as absolute with pointer-events only
on interactive parts.
- Rework the mobile touch handler in terminal.tsx:
* Detect normal vs alternate buffer via term.buffer.active.type instead
of measuring the DOM viewport, which was unreliable in tmux/vim.
* Accumulate swipe distance and emit SGR 1006 mouse-wheel sequences in
steps, so tmux pane scrolling tracks the gesture correctly.
* Prevent default as soon as the swipe is recognized so the page does
not start a competing scroll gesture.
Quality gates: npm run typecheck, npm run lint clean, npm test -- --run 87 passed.
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components
|
||||
|
||||
## role
|
||||
Provides reusable, accessible UI primitives and layout components for the web application, including navigation, feedback states, icons, code display, and authentication guards.
|
||||
Provides reusable, accessible UI components for the web application including layout shell, data states, icons, code display, routing guards, and toast notifications.
|
||||
## parent
|
||||
index: apps/web/src/.pi-map.index.md
|
||||
map: apps/web/src/.pi-map.md
|
||||
|
||||
@@ -4,7 +4,7 @@ dir: apps/web/src/components
|
||||
index: apps/web/src/components/.pi-map.index.md
|
||||
|
||||
## role
|
||||
Provides reusable, accessible UI primitives and layout components for the web application, including navigation, feedback states, icons, code display, and authentication guards.
|
||||
Provides reusable, accessible UI components for the web application including layout shell, data states, icons, code display, routing guards, and toast notifications.
|
||||
## files
|
||||
- app-shell.tsx | Renders the main application shell layout with navigation, header, session management, and mobile-responsive behavior for a React Router-based SPA. | exp: AppShell | dep: react-router-dom, ../api/sessions, ../hooks/use-theme, ../state/auth, ../state/sessions, ../hooks/use-mobile-viewport, ../state/events, ../state/toast, ../state/notifications, ./features/notification/event-toast-bridge, ./features/notification/notification-center, ./icon, ./features/mobile/mobile-nav, ./features/tool/start-tool-fab, ../utils/icons
|
||||
- code-editor.tsx | A React component that renders a syntax-highlighted code editor with line numbers using react-simple-code-editor. | exp: CodeEditor | dep: react, react-simple-code-editor, ../utils/language
|
||||
@@ -17,7 +17,7 @@ Provides reusable, accessible UI primitives and layout components for the web ap
|
||||
- toast-rules.test.ts | Unit tests for mapping instance events to toast notification categories and severities | dep: vitest, ./toast-rules, ../types/events
|
||||
- toast-rules.ts | Maps instance events to toast notifications with deduplication logic to prevent spam | exp: func:mapEventToCategory(event: InstanceEventPayload) → string, call:event.event.startsWith, func:mapEventToSeverity(event: InstanceEventPayload) → "info" | "warning" | "error" | "success", func:handleEventToast(event: InstanceEventPayload) → void, call:shouldShowToast, call:toast.info, call:toast.success, call:toast.warning, call:toast.error, func:clearToastDedup() → void, call:lastToastTime.clear | dep: ../state/toast, ../types/events, toast state module, InstanceEventPayload type
|
||||
## arch
|
||||
Component-based React architecture with functional components, composition patterns, separation of concerns (presentation vs logic), utility-first modules, and test co-location; integrates third-party libraries (Phosphor icons, react-simple-code-editor) with thin abstraction wrappers.
|
||||
Component-based React architecture with functional components, custom hooks for state management, centralized icon mapping, route guards with auth integration, and utility modules for cross-cutting concerns like notifications and data state handling.
|
||||
## tags
|
||||
toast, icon, react, state, code, loading, event, editor
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components/features
|
||||
|
||||
## role
|
||||
Contains reusable React components that implement specific product features and business logic for the web application.
|
||||
Houses reusable React components that implement specific product features and business logic for 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
|
||||
Contains reusable React components that implement specific product features and business logic for the web application.
|
||||
Houses reusable React components that implement specific product features and business logic for the web application.
|
||||
## files
|
||||
## arch
|
||||
Feature-based component organization following domain-driven design principles, likely with co-located components, hooks, and utilities per feature area.
|
||||
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.
|
||||
## tags
|
||||
-
|
||||
## symbols
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
dir: apps/web/src/components/features/terminal
|
||||
|
||||
## role
|
||||
Provides cross-platform terminal UI components for desktop and mobile with session management, special key input, and xterm.js WebSocket connectivity.
|
||||
Provides cross-platform terminal UI components with session management, special key input, and WebSocket-backed xterm.js integration for web-based terminal emulation.
|
||||
## 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 for desktop and mobile with session management, special key input, and xterm.js WebSocket connectivity.
|
||||
Provides cross-platform terminal UI components with session management, special key input, and WebSocket-backed xterm.js integration for web-based terminal emulation.
|
||||
## 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 for desktop and mobile with sessi
|
||||
- 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 | React component that renders an interactive xterm.js terminal with WebSocket connectivity, mobile touch support, flow control, heartbeat monitoring, and reconnection logic. | exp: TerminalProps, TerminalRef, TerminalComponent | dep: react, xterm, xterm-addon-fit, xterm-addon-web-links, xterm/css/xterm.css, ../../../hooks/use-special-keys, React, use-special-keys hook
|
||||
- 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
|
||||
## arch
|
||||
Component-based architecture with platform-specific view variants (Desktop/Mobile), shared session tabs state management, modular special key input panels, and a core terminal component handling WebSocket lifecycle with heartbeat/reconnection logic.
|
||||
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).
|
||||
## tags
|
||||
terminal, special, keys, session, react, tabs, view, renders
|
||||
## symbols
|
||||
|
||||
@@ -356,18 +356,48 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
// 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
|
||||
// whether the viewport has scrollable height.
|
||||
// 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.
|
||||
let touchCleanup: (() => void) | undefined;
|
||||
if (isMobile) {
|
||||
let startY = 0;
|
||||
let startX = 0;
|
||||
let isScrolling = false;
|
||||
let scrollPending = 0;
|
||||
const WHEEL_DISTANCE = 12;
|
||||
|
||||
const flushScroll = (force = false) => {
|
||||
if (scrollPending === 0) return;
|
||||
if (!force && Math.abs(scrollPending) < WHEEL_DISTANCE) return;
|
||||
const direction = Math.sign(scrollPending);
|
||||
const steps = Math.max(
|
||||
1,
|
||||
Math.floor(Math.abs(scrollPending) / WHEEL_DISTANCE),
|
||||
);
|
||||
const ws = wsRef.current;
|
||||
if (ws?.readyState === WebSocket.OPEN && termRef.current) {
|
||||
const buf = termRef.current.buffer.active;
|
||||
const col = buf.cursorX + 1;
|
||||
const row = buf.cursorY + 1;
|
||||
const btn = direction > 0 ? 64 : 65;
|
||||
for (let i = 0; i < steps; i++) {
|
||||
ws.send(`\x1b[<${btn};${col};${row}M`);
|
||||
}
|
||||
}
|
||||
scrollPending = 0;
|
||||
};
|
||||
|
||||
const isNormalBuffer = () => {
|
||||
return termRef.current?.buffer.active.type === "normal";
|
||||
};
|
||||
|
||||
const onTouchStart = (e: TouchEvent) => {
|
||||
if (e.touches.length === 1) {
|
||||
startY = e.touches[0].clientY;
|
||||
startX = e.touches[0].clientX;
|
||||
isScrolling = false;
|
||||
scrollPending = 0;
|
||||
}
|
||||
};
|
||||
const onTouchMove = (e: TouchEvent) => {
|
||||
@@ -381,40 +411,39 @@ export const TerminalComponent = React.forwardRef<TerminalRef, TerminalProps>(
|
||||
}
|
||||
}
|
||||
if (isScrolling) {
|
||||
// Always stop the browser from treating this as a page scroll.
|
||||
e.preventDefault();
|
||||
|
||||
const normalBuffer = isNormalBuffer();
|
||||
const viewport = container.querySelector(
|
||||
".xterm-viewport",
|
||||
) as HTMLElement | null;
|
||||
if (!viewport) return;
|
||||
|
||||
// If the viewport is scrollable, scroll it directly.
|
||||
// Otherwise we are in alternate screen (tmux/vim) and must
|
||||
// send SGR 1006 mouse-wheel protocol data.
|
||||
const hasScrollback = viewport.scrollHeight > viewport.clientHeight;
|
||||
if (hasScrollback) {
|
||||
if (normalBuffer && viewport) {
|
||||
// Normal buffer: scroll the xterm viewport directly.
|
||||
viewport.scrollTop += deltaY;
|
||||
} else {
|
||||
const ws = wsRef.current;
|
||||
if (ws?.readyState === WebSocket.OPEN && termRef.current) {
|
||||
// Use the cursor position as the wheel location so
|
||||
// tmux knows which pane to scroll.
|
||||
const buf = termRef.current.buffer.active;
|
||||
const col = buf.cursorX + 1;
|
||||
const row = buf.cursorY + 1;
|
||||
// SGR 1006: 64 = wheel-up, 65 = wheel-down
|
||||
const btn = deltaY > 0 ? 64 : 65;
|
||||
ws.send(`\x1b[<${btn};${col};${row}M`);
|
||||
}
|
||||
// Alternate screen (tmux/vim): accumulate the swipe and
|
||||
// send SGR 1006 mouse-wheel events in steps.
|
||||
scrollPending += deltaY;
|
||||
flushScroll();
|
||||
}
|
||||
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();
|
||||
}
|
||||
};
|
||||
const onTouchEnd = () => {
|
||||
if (isScrolling) {
|
||||
flushScroll(true);
|
||||
}
|
||||
isScrolling = false;
|
||||
};
|
||||
|
||||
container.addEventListener("touchstart", onTouchStart, {
|
||||
passive: true,
|
||||
passive: false,
|
||||
capture: true,
|
||||
});
|
||||
container.addEventListener("touchmove", onTouchMove, {
|
||||
|
||||
Reference in New Issue
Block a user