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:
Developer
2026-06-13 20:30:52 +00:00
parent 1f2c3dbe2a
commit c9b0259993
18 changed files with 111 additions and 49 deletions
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src
## role
Entry point and core infrastructure for a React web application handling routing, authentication, and shared type definitions.
Frontend web application entry point and core routing infrastructure for a React-based project management or developer workspace platform.
## parent
index: apps/web/.pi-map.index.md
map: apps/web/.pi-map.md
+2 -2
View File
@@ -4,13 +4,13 @@ dir: apps/web/src
index: apps/web/src/.pi-map.index.md
## role
Entry point and core infrastructure for a React web application handling routing, authentication, and shared type definitions.
Frontend web application entry point and core routing infrastructure for a React-based project management or developer workspace platform.
## files
- main.tsx | Bootstraps a React application with routing, authentication, and session management providers. | dep: react, react-dom/client, react-router-dom, ./router, ./state/auth, ./state/sessions, ./styles/tokens.css, ./styles/global.css, ./styles/utilities.css, ./styles/syntax-highlight.css, ./styles/pages/git-history.css, ./styles/pages/projects.css, ./styles/pages/sessions.css, ./styles/pages/ssh-keys.css, ./styles/pages/workspace-detail.css, ./styles/pages/workspaces.css, react-dom
- router.tsx | Defines the React Router configuration for a web application with protected routes, nested layouts, and redirects. | exp: AppRouter | dep: react-router-dom, ./components/app-shell, ./components/protected-route, ./pages/DashboardPage, ./pages/PlaceholderPage, ./pages/ProfilePage, ./pages/ProjectsPage, ./pages/GitRepositoriesPage, ./pages/GitHistoryPage, ./pages/ProjectSettingsPage, ./pages/SettingsPage, ./pages/TerminalPage, ./pages/ToolWorkshopPage, ./pages/SshKeysPage, ./pages/ConfigProfilesPage, ./pages/SessionsPage, ./pages/WorkspacesPage, ./pages/WorkspaceDetailPage
- types.ts | Defines TypeScript type definitions for user sessions, projects, repositories, and workspaces in an application. | exp: SessionUser, SessionPayload, Project, WorkspaceSummary, RepositorySummary, ProjectWithRepos
## arch
Provider composition pattern with React Router v6 (nested routes, layout routes, protected route guards), context-based auth/session management, and centralized TypeScript domain models.
Modern React SPA with declarative routing (React Router v6), protected route guards via authentication/session context providers, nested layout composition, and centralized TypeScript domain models for multi-tenant workspace/project data structures.
## tags
pages, styles, css, router, react, session, dom, project
## symbols
+1 -1
View File
@@ -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
+2 -2
View File
@@ -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
+2 -2
View File
@@ -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, {
+1 -1
View File
@@ -2,7 +2,7 @@
dir: apps/web/src/styles
## role
Provides the complete visual design system and styling foundation for a web application, including themes, utilities, and component-specific styles.
Provides the complete visual design system and styling foundation for the web application, including themes, tokens, utilities, and component-specific styles.
## parent
index: apps/web/src/.pi-map.index.md
map: apps/web/src/.pi-map.md
+3 -3
View File
@@ -4,14 +4,14 @@ dir: apps/web/src/styles
index: apps/web/src/styles/.pi-map.index.md
## role
Provides the complete visual design system and styling foundation for a web application, including themes, utilities, and component-specific styles.
Provides the complete visual design system and styling foundation for the web application, including themes, tokens, utilities, and component-specific styles.
## files
- global.css | Defines global CSS styles for a web application shell layout, navigation, cards, forms, dialogs, settings pages, and responsive design patterns. | dep: CSS custom properties (CSS variables: --border, --panel, --brand, --muted, --ink, --bg, --danger, --success, --warning, --font-size-xs, --font-size-sm, --space-2, --space-3, --space-4, --space-5)
- syntax-highlight.css | Stylesheet for a syntax highlighting component with toolbar, line numbers, code display, and Prism.js theme integration | dep: Prism.js
- tokens.css | Defines a comprehensive CSS design token system with light/dark themes, spacing scales, breakpoints, and fluid typography for a web application.
- utilities.css | Provides a comprehensive responsive CSS utility system with terminal-specific styling, mobile adaptations, and component patterns for a web application. | dep: CSS custom properties (var(--space-*), var(--border), var(--bg), var(--text), var(--brand), var(--muted), var(--success), var(--danger), var(--danger-light)), xterm.js (terminal integration)
- utilities.css | Provides responsive CSS utility classes and component-specific styles for a web application including layout systems, terminal styling, session management, and mobile-first adaptations. | dep: CSS custom properties (var(--space-*), var(--border), var(--bg), var(--text), var(--brand), var(--success), var(--danger), var(--muted)), xterm.js (terminal library)
## arch
Token-based CSS architecture using CSS custom properties for light/dark theming, with layered stylesheets separating tokens, global styles, utilities, and component-specific styling, supporting responsive and fluid design patterns.
Token-driven CSS architecture using CSS custom properties for light/dark theming, utility-first responsive classes, and modular separation of concerns across global styles, syntax highlighting, design tokens, and utility helpers.
## tags
var(, space, global, css, web, application, syntax, defines
## symbols
+37 -4
View File
@@ -551,6 +551,14 @@ a.nav-item,
height: 100%;
padding: 0;
overflow: hidden;
touch-action: none;
overscroll-behavior: none;
}
/* Ensure xterm's internal viewport also disables browser gestures on mobile */
.terminal-container .xterm-viewport {
touch-action: none;
overscroll-behavior: none;
}
/* xterm.js manages its own positioning and sizing */
@@ -564,6 +572,13 @@ a.nav-item,
/* Mobile terminal container - no padding to maximize space */
.terminal-wrapper.mobile .terminal-container {
padding: 0;
touch-action: none;
overscroll-behavior: none;
}
.terminal-wrapper.mobile {
touch-action: none;
overscroll-behavior: none;
}
/* Ensure xterm viewport fills container properly */
@@ -855,17 +870,23 @@ a.nav-item,
Mobile Terminal Overlay
============================================ */
/* Mobile terminal page — no padding, terminal fills viewport */
/* Mobile terminal page — no padding, terminal fills viewport, locked in place */
.terminal-page.mobile {
padding: 0;
gap: 0;
height: 100vh;
height: 100dvh;
position: relative;
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow: hidden;
touch-action: none;
overscroll-behavior: none;
}
/* Overlay status bar — floats over terminal, never resizes it */
/* Overlay status bar — floats over terminal; only its buttons consume pointer events */
.mobile-terminal-overlay {
position: absolute;
top: 0;
@@ -874,11 +895,21 @@ a.nav-item,
z-index: 100;
background: #2d2d2d;
border-bottom: 1px solid #3e3e3e;
pointer-events: none;
transition:
transform 0.3s ease,
opacity 0.3s ease;
}
.mobile-terminal-overlay.visible {
pointer-events: auto;
}
.mobile-terminal-overlay > *,
.mobile-terminal-overlay-tabs {
pointer-events: auto;
}
.mobile-terminal-overlay.hidden {
transform: translateY(-100%);
opacity: 0;
@@ -978,13 +1009,15 @@ a.nav-item,
border-bottom: none;
}
/* Terminal content — always fills full viewport on mobile */
/* Terminal content — always fills full viewport on mobile, locked against page scroll */
.terminal-page-content.mobile-full {
flex: 1;
min-height: 0;
border: none;
border-radius: 0;
overflow: hidden;
touch-action: none;
overscroll-behavior: none;
}
/* Mobile fullscreen */