diff --git a/desktop/src/pages/TerminalSettings.test.tsx b/desktop/src/pages/TerminalSettings.test.tsx index b4bcf76f..fef0757d 100644 --- a/desktop/src/pages/TerminalSettings.test.tsx +++ b/desktop/src/pages/TerminalSettings.test.tsx @@ -108,7 +108,7 @@ describe('TerminalSettings', () => { it('shows a desktop-runtime empty state outside Tauri', () => { render() - expect(screen.getByText(/claude-haha/)).toBeInTheDocument() + expect(screen.getByTestId('settings-terminal-toolbar')).toHaveTextContent('Terminal') expect(screen.getByText('Desktop runtime required')).toBeInTheDocument() expect(terminalMocks.spawn).not.toHaveBeenCalled() }) @@ -127,6 +127,44 @@ describe('TerminalSettings', () => { expect(terminalMocks.fitInstance.fit).toHaveBeenCalled() }) + it('uses one compact toolbar instead of a nested terminal title bar', async () => { + terminalMocks.available = true + + render() + + await waitFor(() => expect(terminalMocks.spawn).toHaveBeenCalled()) + expect(screen.getByTestId('settings-terminal-toolbar')).toHaveTextContent('/bin/zsh') + expect(screen.getByTestId('settings-terminal-frame')).toBeInTheDocument() + expect(screen.queryByText('Host shell')).not.toBeInTheDocument() + }) + + it('lets the settings page keep scrolling when the terminal is not focused', async () => { + terminalMocks.available = true + const container = document.createElement('div') + container.style.overflowY = 'auto' + let scrollTop = 0 + Object.defineProperty(container, 'clientHeight', { configurable: true, value: 100 }) + Object.defineProperty(container, 'scrollHeight', { configurable: true, value: 300 }) + Object.defineProperty(container, 'scrollTop', { + configurable: true, + get: () => scrollTop, + set: (value) => { scrollTop = value }, + }) + const scrollBy = vi.fn(({ top }: ScrollToOptions) => { + scrollTop += Number(top ?? 0) + }) + Object.defineProperty(container, 'scrollBy', { configurable: true, value: scrollBy }) + document.body.appendChild(container) + + render(, { container }) + await waitFor(() => expect(terminalMocks.spawn).toHaveBeenCalled()) + + fireEvent.wheel(screen.getByTestId('settings-terminal-frame'), { deltaY: 48 }) + + expect(scrollBy).toHaveBeenCalledWith({ top: 48, left: 0 }) + expect(scrollTop).toBe(48) + }) + it('starts in the provided cwd when embedded in a project session', async () => { terminalMocks.available = true diff --git a/desktop/src/pages/TerminalSettings.tsx b/desktop/src/pages/TerminalSettings.tsx index 36fcd44f..39da01fc 100644 --- a/desktop/src/pages/TerminalSettings.tsx +++ b/desktop/src/pages/TerminalSettings.tsx @@ -1,4 +1,4 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from 'react' +import { useCallback, useEffect, useMemo, useRef, useState, type WheelEvent } from 'react' import { useTranslation, type TranslationKey } from '../i18n' import { terminalApi } from '../api/terminal' import { useSettingsStore } from '../stores/settingsStore' @@ -26,6 +26,21 @@ const STATUS_LABEL_KEYS: Record = { unavailable: 'settings.terminal.status.unavailable', } +function findScrollableAncestor(element: HTMLElement, deltaY: number): HTMLElement | null { + let parent = element.parentElement + while (parent) { + const style = window.getComputedStyle(parent) + const canScrollY = style.overflowY === 'auto' || style.overflowY === 'scroll' + if (canScrollY && parent.scrollHeight > parent.clientHeight) { + const maxScrollTop = parent.scrollHeight - parent.clientHeight + const canMove = deltaY < 0 ? parent.scrollTop > 0 : parent.scrollTop < maxScrollTop + if (canMove) return parent + } + parent = parent.parentElement + } + return null +} + type TerminalSettingsProps = { active?: boolean cwd?: string @@ -283,6 +298,18 @@ export function TerminalSettings({ runtime.terminal?.clear() } + const handleTerminalWheelCapture = useCallback((event: WheelEvent) => { + const host = hostRef.current + if (!host || host.contains(document.activeElement)) return + + const scroller = findScrollableAncestor(event.currentTarget, event.deltaY) + if (!scroller) return + + event.preventDefault() + event.stopPropagation() + scroller.scrollBy({ top: event.deltaY, left: event.deltaX }) + }, []) + const savePreferences = async () => { setPreferencesError(null) setPreferencesSaved(false) @@ -316,23 +343,33 @@ export function TerminalSettings({ return (
-
-
-

+
+
+
-
+ +
{onOpenInTab && (
-
- - {shellInfo && ( - <> - {shellInfo.shell} - / - {shellInfo.cwd} - - )} -
- {error && (
{error} @@ -492,19 +518,15 @@ export function TerminalSettings({
) : ( -
-
- - - - - {t('settings.terminal.windowTitle')} - -
+
)} @@ -512,7 +534,7 @@ export function TerminalSettings({ ) } -function StatusPill({ status, label }: { status: TerminalStatus; label: string }) { +function StatusPill({ status, label, compact = false }: { status: TerminalStatus; label: string; compact?: boolean }) { const color = status === 'running' ? 'bg-[var(--color-success)]' @@ -523,7 +545,7 @@ function StatusPill({ status, label }: { status: TerminalStatus; label: string } : 'bg-[var(--color-text-tertiary)]' return ( - + {label}