From 9a6b0c4e7289830504f284e1257474930a450c9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A8=8B=E5=BA=8F=E5=91=98=E9=98=BF=E6=B1=9F=28Relakkes?= =?UTF-8?q?=29?= Date: Mon, 25 May 2026 23:20:28 +0800 Subject: [PATCH] fix: compact desktop terminal chrome The terminal page used a header row, a status row, and an inner host-shell chrome, which reduced usable terminal space and made wheel scrolling fall into terminal scrollback while users were navigating Settings. This collapses the terminal metadata and actions into one toolbar, removes the nested host-shell title bar, and forwards wheel input to the surrounding scroll container until the terminal is focused. Constraint: TerminalSettings is shared by Settings, terminal tabs, and docked session terminals. Rejected: Increase only the panel height | leaves duplicate chrome and scroll handoff unchanged Confidence: high Scope-risk: narrow Directive: Keep terminal process lifetime separate from visual chrome; this change is layout and wheel routing only. Tested: cd desktop && bun run test -- src/pages/TerminalSettings.test.tsx src/pages/ActiveSession.test.tsx src/components/layout/ContentRouter.test.tsx Tested: cd desktop && bun run lint Tested: bun run check:desktop Not-tested: Manual Tauri window PTY smoke; browser screenshot unavailable in current tool environment --- desktop/src/pages/TerminalSettings.test.tsx | 40 ++++++++- desktop/src/pages/TerminalSettings.tsx | 90 +++++++++++++-------- 2 files changed, 95 insertions(+), 35 deletions(-) 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}