From b0a3fdf95ffdb961261194d10400267251e42f17 Mon Sep 17 00:00:00 2001 From: PaloSP <32291845+PaloSP@users.noreply.github.com> Date: Wed, 4 Mar 2026 09:41:00 +0100 Subject: [PATCH] feat: add terminal shortcuts panel for mobile (#411) * feat: add terminal shortcuts panel for mobile users Slide-out panel providing touch-friendly shortcut buttons (Esc, Tab, Shift+Tab, Arrow Up/Down) and scroll-to-bottom for the terminal. Integrates into the new modular shell architecture by exposing terminalRef and wsRef from useShellRuntime hook and reusing the existing sendSocketMessage utility. Includes localization keys for en, ja, ko, and zh-CN. * fix: replace dual touch/click handlers with unified pointer events Prevents double-fire on touch devices by removing onTouchEnd handlers and using a single onClick for all interactions (mouse, touch, keyboard). onPointerDown with preventDefault handles focus steal prevention. Also clears pending close timer before scheduling a new one to avoid stale timeout overlap. Addresses CodeRabbit review feedback on PR #411. --------- Co-authored-by: Haileyesus <118998054+blackmammoth@users.noreply.github.com> --- src/components/shell/hooks/useShellRuntime.ts | 2 + src/components/shell/types/types.ts | 2 + src/components/shell/view/Shell.tsx | 9 + .../subcomponents/TerminalShortcutsPanel.tsx | 170 ++++++++++++++++++ src/i18n/locales/en/settings.json | 15 ++ src/i18n/locales/ja/settings.json | 15 ++ src/i18n/locales/ko/settings.json | 15 ++ src/i18n/locales/zh-CN/settings.json | 15 ++ 8 files changed, 243 insertions(+) create mode 100644 src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx diff --git a/src/components/shell/hooks/useShellRuntime.ts b/src/components/shell/hooks/useShellRuntime.ts index 0af6d2f5..ecef3aa3 100644 --- a/src/components/shell/hooks/useShellRuntime.ts +++ b/src/components/shell/hooks/useShellRuntime.ts @@ -149,6 +149,8 @@ export function useShellRuntime({ return { terminalContainerRef, + terminalRef, + wsRef, isConnected, isInitialized, isConnecting, diff --git a/src/components/shell/types/types.ts b/src/components/shell/types/types.ts index b681ad7c..e4bed994 100644 --- a/src/components/shell/types/types.ts +++ b/src/components/shell/types/types.ts @@ -61,6 +61,8 @@ export type ShellSharedRefs = { export type UseShellRuntimeResult = { terminalContainerRef: RefObject; + terminalRef: MutableRefObject; + wsRef: MutableRefObject; isConnected: boolean; isInitialized: boolean; isConnecting: boolean; diff --git a/src/components/shell/view/Shell.tsx b/src/components/shell/view/Shell.tsx index 4fa563b7..822397c9 100644 --- a/src/components/shell/view/Shell.tsx +++ b/src/components/shell/view/Shell.tsx @@ -9,6 +9,7 @@ import ShellConnectionOverlay from './subcomponents/ShellConnectionOverlay'; import ShellEmptyState from './subcomponents/ShellEmptyState'; import ShellHeader from './subcomponents/ShellHeader'; import ShellMinimalView from './subcomponents/ShellMinimalView'; +import TerminalShortcutsPanel from './subcomponents/TerminalShortcutsPanel'; type ShellProps = { selectedProject?: Project | null; @@ -39,6 +40,8 @@ export default function Shell({ const { terminalContainerRef, + terminalRef, + wsRef, isConnected, isInitialized, isConnecting, @@ -157,6 +160,12 @@ export default function Shell({ /> )} + + ); } diff --git a/src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx b/src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx new file mode 100644 index 00000000..3af78c6f --- /dev/null +++ b/src/components/shell/view/subcomponents/TerminalShortcutsPanel.tsx @@ -0,0 +1,170 @@ +import { type MutableRefObject, useState, useCallback, useEffect, useRef } from 'react'; +import { + ChevronLeft, + ChevronRight, + Keyboard, + ArrowDownToLine, +} from 'lucide-react'; +import { useTranslation } from 'react-i18next'; +import type { Terminal } from '@xterm/xterm'; +import { sendSocketMessage } from '../../utils/socket'; + +const SHORTCUTS = [ + { id: 'escape', labelKey: 'escape', sequence: '\x1b', hint: 'Esc' }, + { id: 'tab', labelKey: 'tab', sequence: '\t', hint: 'Tab' }, + { id: 'shift-tab', labelKey: 'shiftTab', sequence: '\x1b[Z', hint: '\u21e7Tab' }, + { id: 'arrow-up', labelKey: 'arrowUp', sequence: '\x1b[A', hint: '\u2191' }, + { id: 'arrow-down', labelKey: 'arrowDown', sequence: '\x1b[B', hint: '\u2193' }, +] as const; + +type TerminalShortcutsPanelProps = { + wsRef: MutableRefObject; + terminalRef: MutableRefObject; + isConnected: boolean; +}; + +const preventFocusSteal = (e: React.PointerEvent) => e.preventDefault(); + +export default function TerminalShortcutsPanel({ + wsRef, + terminalRef, + isConnected, +}: TerminalShortcutsPanelProps) { + const { t } = useTranslation('settings'); + const [isOpen, setIsOpen] = useState(false); + const closeTimeoutRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + } + }; + }, []); + + const handleToggle = useCallback(() => { + setIsOpen((prev) => !prev); + }, []); + + const handleShortcutAction = useCallback((action: () => void) => { + action(); + if (document.activeElement instanceof HTMLElement) { + document.activeElement.blur(); + } + if (closeTimeoutRef.current) { + clearTimeout(closeTimeoutRef.current); + } + closeTimeoutRef.current = setTimeout(() => setIsOpen(false), 50); + }, []); + + const sendInput = useCallback( + (data: string) => { + sendSocketMessage(wsRef.current, { type: 'input', data }); + }, + [wsRef], + ); + + const scrollToBottom = useCallback(() => { + terminalRef.current?.scrollToBottom(); + }, [terminalRef]); + + return ( + <> + {/* Pull Tab */} + + + {/* Panel */} +
+
+ {/* Header */} +
+

+ + {t('terminalShortcuts.title')} +

+
+ + {/* Content — conditionally rendered so buttons remount with clean CSS states */} + {isOpen && ( +
+ {/* Shortcut Keys */} +
+

+ {t('terminalShortcuts.sectionKeys')} +

+ {SHORTCUTS.map((shortcut) => ( + + ))} +
+ + {/* Navigation */} +
+

+ {t('terminalShortcuts.sectionNavigation')} +

+ +
+
+ )} +
+
+ + {/* Backdrop */} + {isOpen && ( +
+ )} + + ); +} diff --git a/src/i18n/locales/en/settings.json b/src/i18n/locales/en/settings.json index 617008a2..2c6a99e1 100644 --- a/src/i18n/locales/en/settings.json +++ b/src/i18n/locales/en/settings.json @@ -83,6 +83,21 @@ } } }, + "terminalShortcuts": { + "title": "Terminal Shortcuts", + "sectionKeys": "Keys", + "sectionNavigation": "Navigation", + "escape": "Escape", + "tab": "Tab", + "shiftTab": "Shift+Tab", + "arrowUp": "Arrow Up", + "arrowDown": "Arrow Down", + "scrollDown": "Scroll Down", + "handle": { + "closePanel": "Close shortcuts panel", + "openPanel": "Open shortcuts panel" + } + }, "mainTabs": { "label": "Settings", "agents": "Agents", diff --git a/src/i18n/locales/ja/settings.json b/src/i18n/locales/ja/settings.json index 05b94408..4fd82ec8 100644 --- a/src/i18n/locales/ja/settings.json +++ b/src/i18n/locales/ja/settings.json @@ -83,6 +83,21 @@ } } }, + "terminalShortcuts": { + "title": "ターミナルショートカット", + "sectionKeys": "キー", + "sectionNavigation": "ナビゲーション", + "escape": "Escape", + "tab": "Tab", + "shiftTab": "Shift+Tab", + "arrowUp": "上矢印", + "arrowDown": "下矢印", + "scrollDown": "下にスクロール", + "handle": { + "closePanel": "ショートカットパネルを閉じる", + "openPanel": "ショートカットパネルを開く" + } + }, "mainTabs": { "label": "設定", "agents": "エージェント", diff --git a/src/i18n/locales/ko/settings.json b/src/i18n/locales/ko/settings.json index bc1074c3..f452291f 100644 --- a/src/i18n/locales/ko/settings.json +++ b/src/i18n/locales/ko/settings.json @@ -83,6 +83,21 @@ } } }, + "terminalShortcuts": { + "title": "터미널 단축키", + "sectionKeys": "키", + "sectionNavigation": "탐색", + "escape": "Escape", + "tab": "Tab", + "shiftTab": "Shift+Tab", + "arrowUp": "위쪽 화살표", + "arrowDown": "아래쪽 화살표", + "scrollDown": "아래로 스크롤", + "handle": { + "closePanel": "단축키 패널 닫기", + "openPanel": "단축키 패널 열기" + } + }, "mainTabs": { "label": "설정", "agents": "에이전트", diff --git a/src/i18n/locales/zh-CN/settings.json b/src/i18n/locales/zh-CN/settings.json index fc910506..cdfb5497 100644 --- a/src/i18n/locales/zh-CN/settings.json +++ b/src/i18n/locales/zh-CN/settings.json @@ -83,6 +83,21 @@ } } }, + "terminalShortcuts": { + "title": "终端快捷键", + "sectionKeys": "按键", + "sectionNavigation": "导航", + "escape": "Escape", + "tab": "Tab", + "shiftTab": "Shift+Tab", + "arrowUp": "上箭头", + "arrowDown": "下箭头", + "scrollDown": "滚动到底部", + "handle": { + "closePanel": "关闭快捷键面板", + "openPanel": "打开快捷键面板" + } + }, "mainTabs": { "label": "设置", "agents": "智能体",