mirror of
https://github.com/wu736139669/hapi.git
synced 2026-08-05 06:24:37 +00:00
fix(web): compact terminal tool cards by default (#601)
This commit is contained in:
Binary file not shown.
|
After Width: | Height: | Size: 760 KiB |
@@ -10,6 +10,7 @@ import { HappyUserMessage } from '@/components/AssistantChat/messages/UserMessag
|
||||
import { HappySystemMessage } from '@/components/AssistantChat/messages/SystemMessage'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Spinner } from '@/components/Spinner'
|
||||
import { useTerminalToolDisplayMode } from '@/hooks/useTerminalToolDisplayMode'
|
||||
import { useTranslation } from '@/lib/use-translation'
|
||||
import { CloseIcon } from '@/components/icons'
|
||||
|
||||
@@ -264,6 +265,7 @@ export function HappyThread(props: {
|
||||
onOutlineItemClick?: (item: ConversationOutlineItem) => void
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const { terminalToolDisplayMode } = useTerminalToolDisplayMode()
|
||||
const viewportRef = useRef<HTMLDivElement | null>(null)
|
||||
const contentRef = useRef<HTMLDivElement | null>(null)
|
||||
const topSentinelRef = useRef<HTMLDivElement | null>(null)
|
||||
@@ -676,6 +678,7 @@ export function HappyThread(props: {
|
||||
api: props.api,
|
||||
sessionId: props.sessionId,
|
||||
metadata: props.metadata,
|
||||
terminalToolDisplayMode,
|
||||
disabled: props.disabled,
|
||||
onRefresh: props.onRefresh,
|
||||
onRetryMessage: props.onRetryMessage
|
||||
|
||||
@@ -1,12 +1,14 @@
|
||||
import type { ReactNode } from 'react'
|
||||
import { createContext, useContext } from 'react'
|
||||
import type { ApiClient } from '@/api/client'
|
||||
import type { TerminalToolDisplayMode } from '@/hooks/useTerminalToolDisplayMode'
|
||||
import type { SessionMetadataSummary } from '@/types/api'
|
||||
|
||||
export type HappyChatContextValue = {
|
||||
api: ApiClient
|
||||
sessionId: string
|
||||
metadata: SessionMetadataSummary | null
|
||||
terminalToolDisplayMode: TerminalToolDisplayMode
|
||||
disabled: boolean
|
||||
onRefresh: () => void
|
||||
onRetryMessage?: (localId: string) => void
|
||||
|
||||
@@ -120,6 +120,7 @@ function HappyNestedBlockList(props: {
|
||||
api={ctx.api}
|
||||
sessionId={ctx.sessionId}
|
||||
metadata={ctx.metadata}
|
||||
terminalToolDisplayMode={ctx.terminalToolDisplayMode}
|
||||
disabled={ctx.disabled}
|
||||
onDone={ctx.onRefresh}
|
||||
block={block}
|
||||
@@ -211,6 +212,7 @@ export function HappyToolMessage(props: ToolCallMessagePartProps) {
|
||||
api={ctx.api}
|
||||
sessionId={ctx.sessionId}
|
||||
metadata={ctx.metadata}
|
||||
terminalToolDisplayMode={ctx.terminalToolDisplayMode}
|
||||
disabled={ctx.disabled}
|
||||
onDone={ctx.onRefresh}
|
||||
block={block}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { shouldShowInlineToolCardBody, shouldUseCompactTerminalToolCard } from '@/components/ToolCard/ToolCard'
|
||||
|
||||
describe('ToolCard terminal display mode helpers', () => {
|
||||
it('treats terminal-related cards as compact by default', () => {
|
||||
expect(shouldUseCompactTerminalToolCard('CodexBash', 'compact')).toBe(true)
|
||||
expect(shouldUseCompactTerminalToolCard('shell_command', 'compact')).toBe(true)
|
||||
expect(shouldUseCompactTerminalToolCard('run_shell_command', 'compact')).toBe(true)
|
||||
expect(shouldUseCompactTerminalToolCard('Read', 'compact')).toBe(false)
|
||||
})
|
||||
|
||||
it('hides inline terminal previews in compact mode', () => {
|
||||
expect(shouldShowInlineToolCardBody('CodexBash', false, 'compact')).toBe(false)
|
||||
})
|
||||
|
||||
it('keeps inline terminal previews in detailed mode', () => {
|
||||
expect(shouldShowInlineToolCardBody('CodexBash', false, 'detailed')).toBe(true)
|
||||
expect(shouldShowInlineToolCardBody('Bash', true, 'detailed')).toBe(true)
|
||||
expect(shouldShowInlineToolCardBody('shell_command', true, 'detailed')).toBe(true)
|
||||
expect(shouldShowInlineToolCardBody('run_shell_command', true, 'detailed')).toBe(true)
|
||||
})
|
||||
|
||||
it('still hides inline bodies for minimal and Task/Agent subagent cards', () => {
|
||||
expect(shouldShowInlineToolCardBody('Task', false, 'detailed')).toBe(false)
|
||||
expect(shouldShowInlineToolCardBody('Agent', false, 'detailed')).toBe(false)
|
||||
expect(shouldShowInlineToolCardBody('Read', true, 'detailed')).toBe(false)
|
||||
})
|
||||
})
|
||||
@@ -17,6 +17,7 @@ import { getToolPresentation } from '@/components/ToolCard/knownTools'
|
||||
import { getToolFullViewComponent, getToolViewComponent } from '@/components/ToolCard/views/_all'
|
||||
import { getToolResultViewComponent } from '@/components/ToolCard/views/_results'
|
||||
import { formatTaskChildLabel, TaskStateIcon } from '@/components/ToolCard/helpers'
|
||||
import type { TerminalToolDisplayMode } from '@/hooks/useTerminalToolDisplayMode'
|
||||
import { usePointerFocusRing } from '@/hooks/usePointerFocusRing'
|
||||
import { getInputString, getInputStringAny, truncate } from '@/lib/toolInputUtils'
|
||||
import { cn } from '@/lib/utils'
|
||||
@@ -25,6 +26,23 @@ import { TraceSection } from '@/components/ToolCard/trace'
|
||||
import { isSubagentToolName } from '@/chat/subagentTool'
|
||||
|
||||
const ELAPSED_INTERVAL_MS = 1000
|
||||
const TERMINAL_RELATED_TOOL_NAMES = new Set(['Bash', 'CodexBash', 'shell_command', 'run_shell_command'])
|
||||
|
||||
export function shouldUseCompactTerminalToolCard(toolName: string, terminalToolDisplayMode: TerminalToolDisplayMode): boolean {
|
||||
return TERMINAL_RELATED_TOOL_NAMES.has(toolName) && terminalToolDisplayMode === 'compact'
|
||||
}
|
||||
|
||||
export function shouldShowInlineToolCardBody(
|
||||
toolName: string,
|
||||
presentationMinimal: boolean,
|
||||
terminalToolDisplayMode: TerminalToolDisplayMode
|
||||
): boolean {
|
||||
if (isSubagentToolName(toolName)) return false
|
||||
if (TERMINAL_RELATED_TOOL_NAMES.has(toolName)) {
|
||||
return terminalToolDisplayMode === 'detailed'
|
||||
}
|
||||
return !presentationMinimal
|
||||
}
|
||||
|
||||
function ElapsedView(props: { from: number; active: boolean }) {
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
@@ -260,6 +278,7 @@ type ToolCardProps = {
|
||||
api: ApiClient
|
||||
sessionId: string
|
||||
metadata: SessionMetadataSummary | null
|
||||
terminalToolDisplayMode: TerminalToolDisplayMode
|
||||
disabled: boolean
|
||||
onDone: () => void
|
||||
block: ToolCallBlock
|
||||
@@ -289,7 +308,9 @@ function ToolCardInner(props: ToolCardProps) {
|
||||
const subtitle = presentation.subtitle ?? props.block.tool.description
|
||||
const taskSummary = renderTaskSummary(props.block, props.metadata, t)
|
||||
const runningFrom = props.block.tool.startedAt ?? props.block.tool.createdAt
|
||||
const showInline = !presentation.minimal && !isSubagentToolName(toolName)
|
||||
const isCodexAgentCard = toolName === 'CodexAgent'
|
||||
const useCompactTerminalCard = shouldUseCompactTerminalToolCard(toolName, props.terminalToolDisplayMode)
|
||||
const showInline = shouldShowInlineToolCardBody(toolName, presentation.minimal, props.terminalToolDisplayMode)
|
||||
const CompactToolView = showInline ? getToolViewComponent(toolName) : null
|
||||
const FullToolView = getToolFullViewComponent(toolName)
|
||||
const ResultToolView = getToolResultViewComponent(toolName)
|
||||
@@ -297,7 +318,6 @@ function ToolCardInner(props: ToolCardProps) {
|
||||
const isAskUserQuestion = isAskUserQuestionToolName(toolName)
|
||||
const isRequestUserInput = isRequestUserInputToolName(toolName)
|
||||
const isQuestionTool = isAskUserQuestion || isRequestUserInput
|
||||
const isCodexAgentCard = toolName === 'CodexAgent'
|
||||
const showsPermissionFooter = Boolean(permission && (
|
||||
permission.status === 'pending'
|
||||
|| ((permission.status === 'denied' || permission.status === 'canceled') && Boolean(permission.reason))
|
||||
@@ -324,7 +344,7 @@ function ToolCardInner(props: ToolCardProps) {
|
||||
{subtitle ? (
|
||||
<CardDescription className={cn(
|
||||
'font-mono text-xs text-[var(--app-tool-card-subtitle)]',
|
||||
isCodexAgentCard ? 'truncate whitespace-nowrap' : 'break-all'
|
||||
isCodexAgentCard || useCompactTerminalCard ? 'truncate whitespace-nowrap' : 'break-all'
|
||||
)}>
|
||||
{truncate(subtitle, 160)}
|
||||
</CardDescription>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DEFAULT_TERMINAL_TOOL_DISPLAY_MODE,
|
||||
getInitialTerminalToolDisplayMode,
|
||||
getTerminalToolDisplayModeOptions,
|
||||
} from './useTerminalToolDisplayMode'
|
||||
|
||||
describe('useTerminalToolDisplayMode helpers', () => {
|
||||
beforeEach(() => {
|
||||
window.localStorage.clear()
|
||||
})
|
||||
|
||||
it('returns the allowed terminal tool display options', () => {
|
||||
expect(getTerminalToolDisplayModeOptions()).toEqual([
|
||||
{ value: 'compact', labelKey: 'settings.chat.terminalToolDisplay.compact' },
|
||||
{ value: 'detailed', labelKey: 'settings.chat.terminalToolDisplay.detailed' },
|
||||
])
|
||||
})
|
||||
|
||||
it('falls back to the default display mode for missing or invalid storage values', () => {
|
||||
expect(getInitialTerminalToolDisplayMode()).toBe(DEFAULT_TERMINAL_TOOL_DISPLAY_MODE)
|
||||
|
||||
window.localStorage.setItem('hapi-terminal-tool-display-mode', 'invalid')
|
||||
expect(getInitialTerminalToolDisplayMode()).toBe(DEFAULT_TERMINAL_TOOL_DISPLAY_MODE)
|
||||
})
|
||||
|
||||
it('reads a valid stored terminal tool display mode', () => {
|
||||
window.localStorage.setItem('hapi-terminal-tool-display-mode', 'detailed')
|
||||
|
||||
expect(getInitialTerminalToolDisplayMode()).toBe('detailed')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,99 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
|
||||
export type TerminalToolDisplayMode = 'compact' | 'detailed'
|
||||
|
||||
export const DEFAULT_TERMINAL_TOOL_DISPLAY_MODE: TerminalToolDisplayMode = 'compact'
|
||||
|
||||
export function getTerminalToolDisplayModeOptions(): ReadonlyArray<{ value: TerminalToolDisplayMode; labelKey: string }> {
|
||||
return [
|
||||
{ value: 'compact', labelKey: 'settings.chat.terminalToolDisplay.compact' },
|
||||
{ value: 'detailed', labelKey: 'settings.chat.terminalToolDisplay.detailed' },
|
||||
]
|
||||
}
|
||||
|
||||
function getTerminalToolDisplayModeStorageKey(): string {
|
||||
return 'hapi-terminal-tool-display-mode'
|
||||
}
|
||||
|
||||
function isBrowser(): boolean {
|
||||
return typeof window !== 'undefined' && typeof document !== 'undefined'
|
||||
}
|
||||
|
||||
function safeGetItem(key: string): string | null {
|
||||
if (!isBrowser()) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
return localStorage.getItem(key)
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function safeSetItem(key: string, value: string): void {
|
||||
if (!isBrowser()) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
localStorage.setItem(key, value)
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
function safeRemoveItem(key: string): void {
|
||||
if (!isBrowser()) {
|
||||
return
|
||||
}
|
||||
try {
|
||||
localStorage.removeItem(key)
|
||||
} catch {
|
||||
// Ignore storage errors
|
||||
}
|
||||
}
|
||||
|
||||
function parseTerminalToolDisplayMode(raw: string | null): TerminalToolDisplayMode {
|
||||
if (raw === 'compact' || raw === 'detailed') {
|
||||
return raw
|
||||
}
|
||||
return DEFAULT_TERMINAL_TOOL_DISPLAY_MODE
|
||||
}
|
||||
|
||||
export function getInitialTerminalToolDisplayMode(): TerminalToolDisplayMode {
|
||||
return parseTerminalToolDisplayMode(safeGetItem(getTerminalToolDisplayModeStorageKey()))
|
||||
}
|
||||
|
||||
export function useTerminalToolDisplayMode(): {
|
||||
terminalToolDisplayMode: TerminalToolDisplayMode
|
||||
setTerminalToolDisplayMode: (mode: TerminalToolDisplayMode) => void
|
||||
} {
|
||||
const [terminalToolDisplayMode, setTerminalToolDisplayModeState] = useState<TerminalToolDisplayMode>(getInitialTerminalToolDisplayMode)
|
||||
|
||||
useEffect(() => {
|
||||
if (!isBrowser()) {
|
||||
return
|
||||
}
|
||||
|
||||
const onStorage = (event: StorageEvent) => {
|
||||
if (event.key !== getTerminalToolDisplayModeStorageKey()) {
|
||||
return
|
||||
}
|
||||
setTerminalToolDisplayModeState(parseTerminalToolDisplayMode(event.newValue))
|
||||
}
|
||||
|
||||
window.addEventListener('storage', onStorage)
|
||||
return () => window.removeEventListener('storage', onStorage)
|
||||
}, [])
|
||||
|
||||
const setTerminalToolDisplayMode = useCallback((mode: TerminalToolDisplayMode) => {
|
||||
setTerminalToolDisplayModeState(mode)
|
||||
|
||||
if (mode === DEFAULT_TERMINAL_TOOL_DISPLAY_MODE) {
|
||||
safeRemoveItem(getTerminalToolDisplayModeStorageKey())
|
||||
} else {
|
||||
safeSetItem(getTerminalToolDisplayModeStorageKey(), mode)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return { terminalToolDisplayMode, setTerminalToolDisplayMode }
|
||||
}
|
||||
@@ -306,6 +306,9 @@ export default {
|
||||
'settings.chat.enterBehavior': 'Enter Key',
|
||||
'settings.chat.enterBehavior.send': 'Send message',
|
||||
'settings.chat.enterBehavior.newline': 'Insert newline',
|
||||
'settings.chat.terminalToolDisplay': 'Terminal Tool Cards',
|
||||
'settings.chat.terminalToolDisplay.compact': 'Compact (command only)',
|
||||
'settings.chat.terminalToolDisplay.detailed': 'Detailed (show output preview)',
|
||||
'settings.voice.title': 'Voice Assistant',
|
||||
'settings.voice.language': 'Voice Language',
|
||||
'settings.voice.autoDetect': 'Auto-detect',
|
||||
|
||||
@@ -308,6 +308,9 @@ export default {
|
||||
'settings.chat.enterBehavior': '回车键行为',
|
||||
'settings.chat.enterBehavior.send': '发送消息',
|
||||
'settings.chat.enterBehavior.newline': '插入换行',
|
||||
'settings.chat.terminalToolDisplay': '终端工具卡片',
|
||||
'settings.chat.terminalToolDisplay.compact': '简洁(仅命令)',
|
||||
'settings.chat.terminalToolDisplay.detailed': '详细(显示输出预览)',
|
||||
'settings.voice.title': '语音助手',
|
||||
'settings.voice.language': '语音语言',
|
||||
'settings.voice.autoDetect': '自动检测',
|
||||
|
||||
@@ -43,6 +43,14 @@ vi.mock('@/hooks/useComposerEnterBehavior', () => ({
|
||||
],
|
||||
}))
|
||||
|
||||
vi.mock('@/hooks/useTerminalToolDisplayMode', () => ({
|
||||
useTerminalToolDisplayMode: () => ({ terminalToolDisplayMode: 'compact', setTerminalToolDisplayMode: vi.fn() }),
|
||||
getTerminalToolDisplayModeOptions: () => [
|
||||
{ value: 'compact', labelKey: 'settings.chat.terminalToolDisplay.compact' },
|
||||
{ value: 'detailed', labelKey: 'settings.chat.terminalToolDisplay.detailed' },
|
||||
],
|
||||
}))
|
||||
|
||||
// Mock useTheme hook
|
||||
vi.mock('@/hooks/useTheme', () => ({
|
||||
useAppearance: () => ({ appearance: 'system', setAppearance: vi.fn() }),
|
||||
@@ -158,11 +166,19 @@ describe('SettingsPage', () => {
|
||||
expect(screen.getAllByText('Send message').length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('renders the Terminal Tool Display setting', () => {
|
||||
renderWithProviders(<SettingsPage />)
|
||||
expect(screen.getAllByText('Terminal Tool Cards').length).toBeGreaterThanOrEqual(1)
|
||||
expect(screen.getAllByText('Compact (command only)').length).toBeGreaterThanOrEqual(1)
|
||||
})
|
||||
|
||||
it('uses correct i18n keys for the Enter Key setting', () => {
|
||||
const spyT = renderWithSpyT(<SettingsPage />)
|
||||
const calledKeys = spyT.mock.calls.map((call) => call[0])
|
||||
expect(calledKeys).toContain('settings.chat.title')
|
||||
expect(calledKeys).toContain('settings.chat.enterBehavior')
|
||||
expect(calledKeys).toContain('settings.chat.enterBehavior.send')
|
||||
expect(calledKeys).toContain('settings.chat.terminalToolDisplay')
|
||||
expect(calledKeys).toContain('settings.chat.terminalToolDisplay.compact')
|
||||
})
|
||||
})
|
||||
|
||||
@@ -5,6 +5,7 @@ import { getElevenLabsSupportedLanguages, getLanguageDisplayName, type Language
|
||||
import { getFontScaleOptions, useFontScale, type FontScale } from '@/hooks/useFontScale'
|
||||
import { getTerminalFontSizeOptions, useTerminalFontSize, type TerminalFontSize } from '@/hooks/useTerminalFontSize'
|
||||
import { getComposerEnterBehaviorOptions, useComposerEnterBehavior, type ComposerEnterBehavior } from '@/hooks/useComposerEnterBehavior'
|
||||
import { getTerminalToolDisplayModeOptions, useTerminalToolDisplayMode, type TerminalToolDisplayMode } from '@/hooks/useTerminalToolDisplayMode'
|
||||
import { useAppearance, getAppearanceOptions, type AppearancePreference } from '@/hooks/useTheme'
|
||||
import { PROTOCOL_VERSION } from '@hapi/protocol'
|
||||
|
||||
@@ -80,16 +81,19 @@ export default function SettingsPage() {
|
||||
const [isFontOpen, setIsFontOpen] = useState(false)
|
||||
const [isTerminalFontOpen, setIsTerminalFontOpen] = useState(false)
|
||||
const [isChatOpen, setIsChatOpen] = useState(false)
|
||||
const [isTerminalToolDisplayOpen, setIsTerminalToolDisplayOpen] = useState(false)
|
||||
const [isVoiceOpen, setIsVoiceOpen] = useState(false)
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const appearanceContainerRef = useRef<HTMLDivElement>(null)
|
||||
const fontContainerRef = useRef<HTMLDivElement>(null)
|
||||
const terminalFontContainerRef = useRef<HTMLDivElement>(null)
|
||||
const chatContainerRef = useRef<HTMLDivElement>(null)
|
||||
const terminalToolDisplayContainerRef = useRef<HTMLDivElement>(null)
|
||||
const voiceContainerRef = useRef<HTMLDivElement>(null)
|
||||
const { fontScale, setFontScale } = useFontScale()
|
||||
const { terminalFontSize, setTerminalFontSize } = useTerminalFontSize()
|
||||
const { composerEnterBehavior, setComposerEnterBehavior } = useComposerEnterBehavior()
|
||||
const { terminalToolDisplayMode, setTerminalToolDisplayMode } = useTerminalToolDisplayMode()
|
||||
const { appearance, setAppearance } = useAppearance()
|
||||
|
||||
// Voice language state - read from localStorage
|
||||
@@ -100,12 +104,14 @@ export default function SettingsPage() {
|
||||
const fontScaleOptions = getFontScaleOptions()
|
||||
const terminalFontSizeOptions = getTerminalFontSizeOptions()
|
||||
const composerEnterBehaviorOptions = getComposerEnterBehaviorOptions()
|
||||
const terminalToolDisplayModeOptions = getTerminalToolDisplayModeOptions()
|
||||
const appearanceOptions = getAppearanceOptions()
|
||||
const currentLocale = locales.find((loc) => loc.value === locale)
|
||||
const currentAppearanceLabel = appearanceOptions.find((opt) => opt.value === appearance)?.labelKey ?? 'settings.display.appearance.system'
|
||||
const currentFontScaleLabel = fontScaleOptions.find((opt) => opt.value === fontScale)?.label ?? '100%'
|
||||
const currentTerminalFontSizeLabel = terminalFontSizeOptions.find((opt) => opt.value === terminalFontSize)?.label ?? '13px'
|
||||
const currentComposerEnterBehaviorLabel = composerEnterBehaviorOptions.find((opt) => opt.value === composerEnterBehavior)?.labelKey ?? 'settings.chat.enterBehavior.send'
|
||||
const currentTerminalToolDisplayModeLabel = terminalToolDisplayModeOptions.find((opt) => opt.value === terminalToolDisplayMode)?.labelKey ?? 'settings.chat.terminalToolDisplay.compact'
|
||||
const currentVoiceLanguage = voiceLanguages.find((lang) => lang.code === voiceLanguage)
|
||||
|
||||
const handleLocaleChange = (newLocale: Locale) => {
|
||||
@@ -133,6 +139,11 @@ export default function SettingsPage() {
|
||||
setIsChatOpen(false)
|
||||
}
|
||||
|
||||
const handleTerminalToolDisplayModeChange = (newMode: TerminalToolDisplayMode) => {
|
||||
setTerminalToolDisplayMode(newMode)
|
||||
setIsTerminalToolDisplayOpen(false)
|
||||
}
|
||||
|
||||
const handleVoiceLanguageChange = (language: Language) => {
|
||||
setVoiceLanguage(language.code)
|
||||
if (language.code === null) {
|
||||
@@ -145,7 +156,7 @@ export default function SettingsPage() {
|
||||
|
||||
// Close dropdown when clicking outside
|
||||
useEffect(() => {
|
||||
if (!isOpen && !isAppearanceOpen && !isFontOpen && !isTerminalFontOpen && !isChatOpen && !isVoiceOpen) return
|
||||
if (!isOpen && !isAppearanceOpen && !isFontOpen && !isTerminalFontOpen && !isChatOpen && !isTerminalToolDisplayOpen && !isVoiceOpen) return
|
||||
|
||||
const handleClickOutside = (event: MouseEvent) => {
|
||||
if (isOpen && containerRef.current && !containerRef.current.contains(event.target as Node)) {
|
||||
@@ -163,6 +174,9 @@ export default function SettingsPage() {
|
||||
if (isChatOpen && chatContainerRef.current && !chatContainerRef.current.contains(event.target as Node)) {
|
||||
setIsChatOpen(false)
|
||||
}
|
||||
if (isTerminalToolDisplayOpen && terminalToolDisplayContainerRef.current && !terminalToolDisplayContainerRef.current.contains(event.target as Node)) {
|
||||
setIsTerminalToolDisplayOpen(false)
|
||||
}
|
||||
if (isVoiceOpen && voiceContainerRef.current && !voiceContainerRef.current.contains(event.target as Node)) {
|
||||
setIsVoiceOpen(false)
|
||||
}
|
||||
@@ -170,11 +184,11 @@ export default function SettingsPage() {
|
||||
|
||||
document.addEventListener('mousedown', handleClickOutside)
|
||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||
}, [isOpen, isAppearanceOpen, isFontOpen, isTerminalFontOpen, isChatOpen, isVoiceOpen])
|
||||
}, [isOpen, isAppearanceOpen, isFontOpen, isTerminalFontOpen, isChatOpen, isTerminalToolDisplayOpen, isVoiceOpen])
|
||||
|
||||
// Close on escape key
|
||||
useEffect(() => {
|
||||
if (!isOpen && !isAppearanceOpen && !isFontOpen && !isTerminalFontOpen && !isChatOpen && !isVoiceOpen) return
|
||||
if (!isOpen && !isAppearanceOpen && !isFontOpen && !isTerminalFontOpen && !isChatOpen && !isTerminalToolDisplayOpen && !isVoiceOpen) return
|
||||
|
||||
const handleEscape = (event: KeyboardEvent) => {
|
||||
if (event.key === 'Escape') {
|
||||
@@ -183,13 +197,14 @@ export default function SettingsPage() {
|
||||
setIsFontOpen(false)
|
||||
setIsTerminalFontOpen(false)
|
||||
setIsChatOpen(false)
|
||||
setIsTerminalToolDisplayOpen(false)
|
||||
setIsVoiceOpen(false)
|
||||
}
|
||||
}
|
||||
|
||||
document.addEventListener('keydown', handleEscape)
|
||||
return () => document.removeEventListener('keydown', handleEscape)
|
||||
}, [isOpen, isAppearanceOpen, isFontOpen, isTerminalFontOpen, isChatOpen, isVoiceOpen])
|
||||
}, [isOpen, isAppearanceOpen, isFontOpen, isTerminalFontOpen, isChatOpen, isTerminalToolDisplayOpen, isVoiceOpen])
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
@@ -467,6 +482,54 @@ export default function SettingsPage() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div ref={terminalToolDisplayContainerRef} className="relative">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setIsTerminalToolDisplayOpen(!isTerminalToolDisplayOpen)}
|
||||
className="flex w-full items-center justify-between px-3 py-3 text-left transition-colors hover:bg-[var(--app-subtle-bg)]"
|
||||
aria-expanded={isTerminalToolDisplayOpen}
|
||||
aria-haspopup="listbox"
|
||||
>
|
||||
<span className="text-[var(--app-fg)]">{t('settings.chat.terminalToolDisplay')}</span>
|
||||
<span className="flex items-center gap-1 text-[var(--app-hint)]">
|
||||
<span>{t(currentTerminalToolDisplayModeLabel)}</span>
|
||||
<ChevronDownIcon className={`transition-transform ${isTerminalToolDisplayOpen ? 'rotate-180' : ''}`} />
|
||||
</span>
|
||||
</button>
|
||||
|
||||
{isTerminalToolDisplayOpen && (
|
||||
<div
|
||||
className="absolute right-3 top-full mt-1 min-w-[230px] rounded-lg border border-[var(--app-border)] bg-[var(--app-bg)] shadow-lg overflow-hidden z-50"
|
||||
role="listbox"
|
||||
aria-label={t('settings.chat.terminalToolDisplay')}
|
||||
>
|
||||
{terminalToolDisplayModeOptions.map((opt) => {
|
||||
const isSelected = terminalToolDisplayMode === opt.value
|
||||
return (
|
||||
<button
|
||||
key={opt.value}
|
||||
type="button"
|
||||
role="option"
|
||||
aria-selected={isSelected}
|
||||
onClick={() => handleTerminalToolDisplayModeChange(opt.value)}
|
||||
className={`flex items-center justify-between w-full px-3 py-2 text-base text-left transition-colors ${
|
||||
isSelected
|
||||
? 'text-[var(--app-link)] bg-[var(--app-subtle-bg)]'
|
||||
: 'text-[var(--app-fg)] hover:bg-[var(--app-subtle-bg)]'
|
||||
}`}
|
||||
>
|
||||
<span>{t(opt.labelKey)}</span>
|
||||
{isSelected && (
|
||||
<span className="ml-2 text-[var(--app-link)]">
|
||||
<CheckIcon />
|
||||
</span>
|
||||
)}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Voice Assistant section */}
|
||||
|
||||
Reference in New Issue
Block a user