- Add mobile viewport detection hook - Add virtual keyboard detection with fallback - Add auto-hide hook for header/keys strip - Add special keys mapping hook - Create MobileTerminalHeader, SpecialKeysStrip, SpecialKeysPanel components - Create MobileTerminalWrapper component - Update TerminalComponent with mobile support, font scaling, copy/paste, reconnection - Update AppShell to hide chrome on mobile terminal pages - Update TerminalPage to use MobileTerminalWrapper - Add comprehensive mobile terminal styles - TypeScript check passes - Build succeeds
5.7 KiB
Context
The current terminal implementation (apps/web/src/components/terminal.tsx) uses xterm.js with a fixed header and minimal mobile considerations. The terminal page (apps/web/src/pages/terminal.tsx) renders inside the standard AppShell layout (apps/web/src/components/app-shell.tsx), which consumes significant viewport space on mobile devices.
On mobile devices (viewport < 768px):
- The virtual keyboard covers 40-50% of the screen
- xterm.js touch events conflict with browser touch behavior
- Special keys (Ctrl, Esc, Tab, Arrows) are not available on mobile keyboards
- The AppShell header and sidebar waste precious screen real estate
- No mechanism exists to handle virtual keyboard appearance/disappearance
Goals / Non-Goals
Goals:
- Make terminal sessions practical on mobile devices for occasional use
- Provide access to special terminal keys without external keyboard
- Maximize terminal screen real estate on mobile
- Handle virtual keyboard gracefully
- Support full terminal functionality (vim, tmux, etc.)
Non-Goals:
- Native mobile app (stays web-based)
- Command palette / quick commands (future enhancement)
- Offline terminal access
- Mobile-first redesign of the entire application (terminal pages only)
- Gesture-based text selection (use xterm.js native)
Decisions
Decision: Collapsible AppShell, Not Hidden
Choice: Collapse AppShell to a minimal auto-hiding header instead of completely hiding it.
Rationale: Users need a way to navigate back and access the menu. Complete removal would trap users in the terminal page.
Alternative considered: Fullscreen mode with swipe-from-edge to reveal nav. Rejected because it's not discoverable and conflicts with browser gestures.
Decision: Auto-Hide Header and Keys Strip
Choice: Both header and special keys strip auto-hide after 3 seconds of inactivity.
Rationale: Maximizes terminal space while keeping controls accessible. Tap to toggle visibility is intuitive.
Alternative considered: Always-visible fixed bars. Rejected because they permanently reduce terminal height by ~20%.
Decision: Hidden Input for Keyboard Management
Choice: Use a hidden/transparent input element to maintain virtual keyboard focus.
Rationale: xterm.js handles keyboard input directly, but mobile browsers need a focused input to show the virtual keyboard. A hidden input bridges this gap without interfering with xterm.js rendering.
Alternative considered: Custom on-screen keyboard. Rejected because native virtual keyboards provide better UX (autocorrect, swipe typing, user's preferred keyboard layout).
Decision: Special Keys as Bottom Strip, Not Floating
Choice: Fixed bottom strip that slides up, not floating action buttons.
Rationale: Bottom placement is thumb-friendly and doesn't obscure terminal content. Fixed position makes it always accessible.
Alternative considered: Floating action button that expands to a menu. Rejected because it requires two taps for every special key.
Decision: Debounced Resize (250ms)
Choice: 250ms debounce for resize events.
Rationale: Mobile keyboard animation is slow and produces multiple resize events. 250ms catches the final state without being sluggish.
Alternative considered: No debounce (immediate resize). Rejected because it causes excessive xterm.js refits and WebSocket resize messages.
Decision: No New Dependencies
Choice: Implement using existing React, xterm.js, and browser APIs.
Rationale: All required functionality (touch events, viewport API, clipboard) is available natively. Adding libraries increases bundle size for a feature used occasionally.
Alternative considered: react-use hooks, xterm-addon-webgl. Rejected to keep bundle size down.
Risks / Trade-offs
[Risk] Visual Viewport API unreliability → Mitigation: Implement fallback using window.innerHeight comparison and focus-based detection. Accept imperfect behavior on older browsers.
[Risk] xterm.js touch conflicts → Mitigation: Use touch-action: none on terminal container. Let xterm.js handle its own touch events. Disable browser zoom to prevent pinch conflicts.
[Risk] WebSocket drops on network change/backgrounding → Mitigation: Implement reconnect logic with exponential backoff. Show clear status to user. Document that mobile networks may cause disconnections.
[Risk] Clipboard API restrictions on mobile Safari → Mitigation: Use both modern Clipboard API and document.execCommand('copy') fallback. Show user feedback on failure.
[Risk] Screen rotation causes layout flicker → Mitigation: Debounced resize. CSS transitions on layout changes. Consider orientation lock prompt for landscape preference.
[Trade-off] Touch targets vs terminal density → Larger touch targets mean fewer terminal cells visible. Compromise: 16px minimum font size provides readable text while keeping reasonable cell count.
Migration Plan
No migration needed. This is a purely additive frontend change that doesn't affect data models, APIs, or existing desktop behavior. Desktop terminal experience remains unchanged.
Deployment:
- Merge changes to dev branch
- Verify on actual mobile devices (iOS Safari, Android Chrome)
- Monitor for any desktop regressions
Rollback: Revert frontend commit. No database or API changes involved.
Open Questions
- Should we implement a landscape orientation prompt? ("Rotate for better experience")
- Should font size preference sync across devices (via backend user config) or stay local?
- What's the maximum number of special keys to show in the primary strip before requiring "More"?
- Should the header show connection status, or is the terminal's own status dot sufficient?