import { useState, useEffect, useRef, forwardRef, useImperativeHandle, } from 'react'; import { Box, ScrollArea, ActionIcon } from '@mantine/core'; import { ChevronDown } from 'lucide-react'; import { SERVER_READY_SIGNALS, STATUSBAR_HEIGHT, TITLEBAR_HEIGHT, } from '@/constants'; import { handleTerminalOutput, processTerminalContent } from '@/utils/terminal'; import { useLaunchConfigStore } from '@/stores/launchConfig'; import { usePreferencesStore } from '@/stores/preferences'; interface TerminalTabProps { onServerReady: (url: string) => void; } export interface TerminalTabRef { scrollToBottom: () => void; } export const TerminalTab = forwardRef( ({ onServerReady }, ref) => { const { host, port, isImageGenerationMode } = useLaunchConfigStore(); const { frontendPreference, resolvedColorScheme: colorScheme } = usePreferencesStore(); const [terminalContent, setTerminalContent] = useState(''); const [isUserScrolling, setIsUserScrolling] = useState(false); const [shouldAutoScroll, setShouldAutoScroll] = useState(true); const [isVisible, setIsVisible] = useState(false); const scrollAreaRef = useRef(null); const viewportRef = useRef(null); const lastScrollTop = useRef(0); useEffect(() => { const timer = setTimeout(() => { setIsVisible(true); }, 150); return () => clearTimeout(timer); }, []); const handleScroll = ({ y }: { y: number }) => { if (!viewportRef.current) return; const { scrollHeight, clientHeight } = viewportRef.current; const isAtBottomNow = y + clientHeight >= scrollHeight - 10; if (y < lastScrollTop.current) { setIsUserScrolling(true); setShouldAutoScroll(false); } else if (isAtBottomNow) { setIsUserScrolling(false); setShouldAutoScroll(true); } lastScrollTop.current = y; }; useEffect(() => { if (shouldAutoScroll && !isUserScrolling && viewportRef.current) { const viewport = viewportRef.current; viewport.scrollTop = viewport.scrollHeight; } }, [terminalContent, shouldAutoScroll, isUserScrolling]); useEffect(() => { const cleanup = window.electronAPI.kobold.onKoboldOutput( (data: string) => { setTerminalContent((prev) => { const newData = data.toString(); if (onServerReady) { const serverHost = host || 'localhost'; const serverPort = port || 5001; let signalToCheck: string = SERVER_READY_SIGNALS.KOBOLDCPP; if (frontendPreference === 'sillytavern') { signalToCheck = SERVER_READY_SIGNALS.SILLYTAVERN; } else if (frontendPreference === 'openwebui') { signalToCheck = SERVER_READY_SIGNALS.OPENWEBUI; } else if ( frontendPreference === 'comfyui' && isImageGenerationMode ) { signalToCheck = SERVER_READY_SIGNALS.COMFYUI; } if (newData.includes(signalToCheck)) { setTimeout( () => onServerReady(`http://${serverHost}:${serverPort}`), 1500 ); } } return handleTerminalOutput(prev, newData); }); } ); return cleanup; }, [onServerReady, host, port, frontendPreference, isImageGenerationMode]); const scrollToBottom = () => { if (viewportRef.current) { const viewport = viewportRef.current; viewport.scrollTop = viewport.scrollHeight; setShouldAutoScroll(true); setIsUserScrolling(false); } }; useImperativeHandle(ref, () => ({ scrollToBottom, })); return (
{isUserScrolling && !shouldAutoScroll && ( )} ); } ); TerminalTab.displayName = 'TerminalTab';