From b6bda3d6920f018713139447a1076cc8d8eca667 Mon Sep 17 00:00:00 2001 From: Fusion Date: Sun, 24 May 2026 11:30:04 +0200 Subject: [PATCH] feat: implement mobile terminal UX - 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 --- apps/web/src/components/app-shell.tsx | 42 +- .../src/components/mobile-terminal-header.tsx | 72 ++++ .../components/mobile-terminal-wrapper.tsx | 100 +++++ .../web/src/components/special-keys-panel.tsx | 84 ++++ .../web/src/components/special-keys-strip.tsx | 53 +++ apps/web/src/components/terminal.tsx | 366 +++++++++++++---- apps/web/src/hooks/use-auto-hide.ts | 68 ++++ apps/web/src/hooks/use-mobile-viewport.ts | 21 + apps/web/src/hooks/use-special-keys.ts | 80 ++++ apps/web/src/hooks/use-virtual-keyboard.ts | 68 ++++ apps/web/src/pages/terminal.tsx | 16 +- apps/web/src/styles.css | 368 ++++++++++++++++++ .../changes/mobile-terminal-ux/.openspec.yaml | 2 + openspec/changes/mobile-terminal-ux/design.md | 108 +++++ .../changes/mobile-terminal-ux/proposal.md | 30 ++ .../specs/frontend-foundation/spec.md | 23 ++ .../specs/mobile-terminal-ux/spec.md | 121 ++++++ .../specs/tool-terminal/spec.md | 123 ++++++ openspec/changes/mobile-terminal-ux/tasks.md | 57 +++ 19 files changed, 1721 insertions(+), 81 deletions(-) create mode 100644 apps/web/src/components/mobile-terminal-header.tsx create mode 100644 apps/web/src/components/mobile-terminal-wrapper.tsx create mode 100644 apps/web/src/components/special-keys-panel.tsx create mode 100644 apps/web/src/components/special-keys-strip.tsx create mode 100644 apps/web/src/hooks/use-auto-hide.ts create mode 100644 apps/web/src/hooks/use-mobile-viewport.ts create mode 100644 apps/web/src/hooks/use-special-keys.ts create mode 100644 apps/web/src/hooks/use-virtual-keyboard.ts create mode 100644 openspec/changes/mobile-terminal-ux/.openspec.yaml create mode 100644 openspec/changes/mobile-terminal-ux/design.md create mode 100644 openspec/changes/mobile-terminal-ux/proposal.md create mode 100644 openspec/changes/mobile-terminal-ux/specs/frontend-foundation/spec.md create mode 100644 openspec/changes/mobile-terminal-ux/specs/mobile-terminal-ux/spec.md create mode 100644 openspec/changes/mobile-terminal-ux/specs/tool-terminal/spec.md create mode 100644 openspec/changes/mobile-terminal-ux/tasks.md diff --git a/apps/web/src/components/app-shell.tsx b/apps/web/src/components/app-shell.tsx index 8636e49..d01b798 100644 --- a/apps/web/src/components/app-shell.tsx +++ b/apps/web/src/components/app-shell.tsx @@ -1,11 +1,12 @@ -import { useCallback, useEffect } from "react"; -import { Link, NavLink, Outlet } from "react-router-dom"; +import { useCallback, useEffect, useState } from "react"; +import { Link, NavLink, Outlet, useLocation } from "react-router-dom"; import { getUserSessions } from "../api/sessions"; import type { Session } from "../api/sessions"; import { useTheme } from "../hooks/use-theme"; import { useAuth } from "../state/auth"; import { useSessions } from "../state/sessions"; +import { useMobileViewport } from "../hooks/use-mobile-viewport"; import { Icon } from "./icon"; import type { IconName } from "../utils/icons"; @@ -39,6 +40,11 @@ export const AppShell = () => { useTheme(); const { user, logout } = useAuth(); const { sessions, setAllSessions } = useSessions(); + const location = useLocation(); + const isMobile = useMobileViewport(); + const [mobileMenuOpen, setMobileMenuOpen] = useState(false); + + const isMobileTerminal = isMobile && location.pathname.includes("/instances/") && location.pathname.includes("/terminal"); const loadSessions = useCallback(async () => { try { @@ -58,6 +64,19 @@ export const AppShell = () => { return () => clearInterval(interval); }, [loadSessions]); + // Close mobile menu on route change + useEffect(() => { + setMobileMenuOpen(false); + }, [location.pathname]); + + if (isMobileTerminal) { + return ( +
+ +
+ ); + } + return (
@@ -82,7 +101,17 @@ export const AppShell = () => {
-