diff --git a/packages/client/src/components/hermes/chat/DrawerPanel.vue b/packages/client/src/components/hermes/chat/DrawerPanel.vue index a25e63f..42a146f 100644 --- a/packages/client/src/components/hermes/chat/DrawerPanel.vue +++ b/packages/client/src/components/hermes/chat/DrawerPanel.vue @@ -86,9 +86,10 @@ function handleClose() { .drawer-panel { position: fixed; top: 0; - right: -900px; - width: 900px; - height: 100vh; + right: min(-1180px, -88vw); + width: min(1180px, 88vw); + height: calc(100 * var(--vh)); + max-height: calc(100 * var(--vh)); background: $bg-card; box-shadow: -2px 0 8px rgba(0, 0, 0, 0.15); display: flex; @@ -180,5 +181,3 @@ function handleClose() { overflow: auto; } - - diff --git a/packages/client/src/components/hermes/chat/TerminalPanel.vue b/packages/client/src/components/hermes/chat/TerminalPanel.vue index c64a09c..cfe83a3 100644 --- a/packages/client/src/components/hermes/chat/TerminalPanel.vue +++ b/packages/client/src/components/hermes/chat/TerminalPanel.vue @@ -103,6 +103,9 @@ let activeFitAddon: FitAddon | null = null; let resizeObserver: ResizeObserver | null = null; let reconnectAttempts = 0; const MAX_RECONNECT_ATTEMPTS = 3; +let touchScrollLastY: number | null = null; +let touchScrollRemainder = 0; +const TOUCH_SCROLL_LINE_PX = 18; // ─── Computed ────────────────────────────────────────────────── @@ -356,6 +359,35 @@ function sendResize() { } catch {} } +function handleTerminalTouchStart(event: TouchEvent) { + if (event.touches.length !== 1) { + touchScrollLastY = null; + touchScrollRemainder = 0; + return; + } + touchScrollLastY = event.touches[0].clientY; + touchScrollRemainder = 0; +} + +function handleTerminalTouchMove(event: TouchEvent) { + if (!activeTerm || event.touches.length !== 1 || touchScrollLastY === null) return; + const nextY = event.touches[0].clientY; + touchScrollRemainder += touchScrollLastY - nextY; + touchScrollLastY = nextY; + + const lines = Math.trunc(touchScrollRemainder / TOUCH_SCROLL_LINE_PX); + if (lines === 0) return; + + activeTerm.scrollLines(lines); + touchScrollRemainder -= lines * TOUCH_SCROLL_LINE_PX; + event.preventDefault(); +} + +function handleTerminalTouchEnd() { + touchScrollLastY = null; + touchScrollRemainder = 0; +} + // ─── Theme ─────────────────────────────────────────────────────── function applyTheme(themeName: string) { @@ -517,7 +549,15 @@ onUnmounted(() => {
-
+
@@ -798,4 +838,35 @@ onUnmounted(() => { display: none !important; } } + +@media (max-width: $breakpoint-mobile) { + .terminal-panel-drawer { + height: calc(100 * var(--vh)); + max-height: calc(100 * var(--vh)); + } + + .terminal-main { + min-height: 0; + } + + .terminal-container { + margin-bottom: calc(8px + env(safe-area-inset-bottom, 0px)); + } + + .terminal-xterm { + :deep(.xterm-viewport), + :deep(.xterm-scrollable-element) { + touch-action: pan-y; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + scrollbar-width: thin !important; + } + + :deep(.xterm-viewport::-webkit-scrollbar), + :deep(.xterm-scrollable-element::-webkit-scrollbar) { + display: block !important; + width: 6px !important; + } + } +} diff --git a/packages/client/src/views/hermes/TerminalView.vue b/packages/client/src/views/hermes/TerminalView.vue index 4a51fc3..f00b00e 100644 --- a/packages/client/src/views/hermes/TerminalView.vue +++ b/packages/client/src/views/hermes/TerminalView.vue @@ -245,6 +245,9 @@ let activeTerm: Terminal | null = null; let activeFitAddon: FitAddon | null = null; let resizeObserver: ResizeObserver | null = null; let mobileQuery: MediaQueryList | null = null; +let touchScrollLastY: number | null = null; +let touchScrollRemainder = 0; +const TOUCH_SCROLL_LINE_PX = 18; // ─── Computed ────────────────────────────────────────────────── @@ -495,6 +498,35 @@ function sendResize() { } catch {} } +function handleTerminalTouchStart(event: TouchEvent) { + if (event.touches.length !== 1) { + touchScrollLastY = null; + touchScrollRemainder = 0; + return; + } + touchScrollLastY = event.touches[0].clientY; + touchScrollRemainder = 0; +} + +function handleTerminalTouchMove(event: TouchEvent) { + if (!activeTerm || event.touches.length !== 1 || touchScrollLastY === null) return; + const nextY = event.touches[0].clientY; + touchScrollRemainder += touchScrollLastY - nextY; + touchScrollLastY = nextY; + + const lines = Math.trunc(touchScrollRemainder / TOUCH_SCROLL_LINE_PX); + if (lines === 0) return; + + activeTerm.scrollLines(lines); + touchScrollRemainder -= lines * TOUCH_SCROLL_LINE_PX; + event.preventDefault(); +} + +function handleTerminalTouchEnd() { + touchScrollLastY = null; + touchScrollRemainder = 0; +} + // ─── Theme ─────────────────────────────────────────────────────── function applyTheme(themeName: string) { @@ -689,7 +721,15 @@ onUnmounted(() => {
-
+
@@ -980,6 +1020,19 @@ onUnmounted(() => { // ─── Mobile ───────────────────────────────────────────────────── @media (max-width: $breakpoint-mobile) { + .terminal-panel { + height: calc(100 * var(--vh)); + max-height: calc(100 * var(--vh)); + } + + .terminal-main { + min-height: 0; + } + + .terminal-container { + margin-bottom: calc(8px + env(safe-area-inset-bottom, 0px)); + } + .session-close-btn { display: flex; } @@ -1011,6 +1064,20 @@ onUnmounted(() => { left: 0; right: 0; bottom: 0; + + :deep(.xterm-viewport), + :deep(.xterm-scrollable-element) { + touch-action: pan-y; + -webkit-overflow-scrolling: touch; + overscroll-behavior: contain; + scrollbar-width: thin !important; + } + + :deep(.xterm-viewport::-webkit-scrollbar), + :deep(.xterm-scrollable-element::-webkit-scrollbar) { + display: block !important; + width: 6px !important; + } } }