From bc2f488902c483f500f9e3e6c064f401cb79fdae Mon Sep 17 00:00:00 2001 From: Haileyesus Date: Tue, 24 Feb 2026 14:55:06 +0300 Subject: [PATCH] fix(shell): remove Shell.jsx and add provider fallback from a previous commit --- src/components/Shell.jsx | 692 ------------------ .../shell/hooks/useShellConnection.ts | 4 +- 2 files changed, 1 insertion(+), 695 deletions(-) delete mode 100644 src/components/Shell.jsx diff --git a/src/components/Shell.jsx b/src/components/Shell.jsx deleted file mode 100644 index 44115b9..0000000 --- a/src/components/Shell.jsx +++ /dev/null @@ -1,692 +0,0 @@ -import React, { useEffect, useRef, useState, useCallback, useMemo } from 'react'; -import { Terminal } from '@xterm/xterm'; -import { FitAddon } from '@xterm/addon-fit'; -import { WebglAddon } from '@xterm/addon-webgl'; -import { WebLinksAddon } from '@xterm/addon-web-links'; -import '@xterm/xterm/css/xterm.css'; -import { useTranslation } from 'react-i18next'; -import { IS_PLATFORM } from '../constants/config'; - -const xtermStyles = ` - .xterm .xterm-screen { - outline: none !important; - } - .xterm:focus .xterm-screen { - outline: none !important; - } - .xterm-screen:focus { - outline: none !important; - } -`; - -if (typeof document !== 'undefined') { - const styleSheet = document.createElement('style'); - styleSheet.type = 'text/css'; - styleSheet.innerText = xtermStyles; - document.head.appendChild(styleSheet); -} - -function fallbackCopyToClipboard(text) { - if (!text || typeof document === 'undefined') return false; - - const textarea = document.createElement('textarea'); - textarea.value = text; - textarea.setAttribute('readonly', ''); - textarea.style.position = 'fixed'; - textarea.style.opacity = '0'; - textarea.style.pointerEvents = 'none'; - document.body.appendChild(textarea); - textarea.focus(); - textarea.select(); - - let copied = false; - try { - copied = document.execCommand('copy'); - } catch { - copied = false; - } finally { - document.body.removeChild(textarea); - } - - return copied; -} - -const CODEX_DEVICE_AUTH_URL = 'https://auth.openai.com/codex/device'; - -function isCodexLoginCommand(command) { - return typeof command === 'string' && /\bcodex\s+login\b/i.test(command); -} - -function Shell({ selectedProject, selectedSession, initialCommand, isPlainShell = false, onProcessComplete, minimal = false, autoConnect = false }) { - const { t } = useTranslation('chat'); - const terminalRef = useRef(null); - const terminal = useRef(null); - const fitAddon = useRef(null); - const ws = useRef(null); - const [isConnected, setIsConnected] = useState(false); - const [isInitialized, setIsInitialized] = useState(false); - const [isRestarting, setIsRestarting] = useState(false); - const [lastSessionId, setLastSessionId] = useState(null); - const [isConnecting, setIsConnecting] = useState(false); - const [authUrl, setAuthUrl] = useState(''); - const [authUrlCopyStatus, setAuthUrlCopyStatus] = useState('idle'); - const [isAuthPanelHidden, setIsAuthPanelHidden] = useState(false); - - const selectedProjectRef = useRef(selectedProject); - const selectedSessionRef = useRef(selectedSession); - const initialCommandRef = useRef(initialCommand); - const isPlainShellRef = useRef(isPlainShell); - const onProcessCompleteRef = useRef(onProcessComplete); - const authUrlRef = useRef(''); - - useEffect(() => { - selectedProjectRef.current = selectedProject; - selectedSessionRef.current = selectedSession; - initialCommandRef.current = initialCommand; - isPlainShellRef.current = isPlainShell; - onProcessCompleteRef.current = onProcessComplete; - }); - - const openAuthUrlInBrowser = useCallback((url = authUrlRef.current) => { - if (!url) return false; - - const popup = window.open(url, '_blank', 'noopener,noreferrer'); - if (popup) { - try { - popup.opener = null; - } catch { - // Ignore cross-origin restrictions when trying to null opener - } - return true; - } - - return false; - }, []); - - const copyAuthUrlToClipboard = useCallback(async (url = authUrlRef.current) => { - if (!url) return false; - - let copied = false; - try { - if (typeof navigator !== 'undefined' && navigator.clipboard?.writeText) { - await navigator.clipboard.writeText(url); - copied = true; - } - } catch { - copied = false; - } - - if (!copied) { - copied = fallbackCopyToClipboard(url); - } - - return copied; - }, []); - - const connectWebSocket = useCallback(async () => { - if (isConnecting || isConnected) return; - - try { - let wsUrl; - - if (IS_PLATFORM) { - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - wsUrl = `${protocol}//${window.location.host}/shell`; - } else { - const token = localStorage.getItem('auth-token'); - if (!token) { - console.error('No authentication token found for Shell WebSocket connection'); - return; - } - - const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; - wsUrl = `${protocol}//${window.location.host}/shell?token=${encodeURIComponent(token)}`; - } - - ws.current = new WebSocket(wsUrl); - - ws.current.onopen = () => { - setIsConnected(true); - setIsConnecting(false); - authUrlRef.current = ''; - setAuthUrl(''); - setAuthUrlCopyStatus('idle'); - setIsAuthPanelHidden(false); - - setTimeout(() => { - if (fitAddon.current && terminal.current) { - fitAddon.current.fit(); - - ws.current.send(JSON.stringify({ - type: 'init', - projectPath: selectedProjectRef.current.fullPath || selectedProjectRef.current.path, - sessionId: isPlainShellRef.current ? null : selectedSessionRef.current?.id, - hasSession: isPlainShellRef.current ? false : !!selectedSessionRef.current, - provider: isPlainShellRef.current ? 'plain-shell' : (selectedSessionRef.current?.__provider || localStorage.getItem('selected-provider') || 'claude'), - cols: terminal.current.cols, - rows: terminal.current.rows, - initialCommand: initialCommandRef.current, - isPlainShell: isPlainShellRef.current - })); - } - }, 100); - }; - - ws.current.onmessage = (event) => { - try { - const data = JSON.parse(event.data); - - if (data.type === 'output') { - let output = data.data; - - if (isPlainShellRef.current && onProcessCompleteRef.current) { - const cleanOutput = output.replace(/\x1b\[[0-9;]*m/g, ''); - if (cleanOutput.includes('Process exited with code 0')) { - onProcessCompleteRef.current(0); - } else if (cleanOutput.match(/Process exited with code (\d+)/)) { - const exitCode = parseInt(cleanOutput.match(/Process exited with code (\d+)/)[1]); - if (exitCode !== 0) { - onProcessCompleteRef.current(exitCode); - } - } - } - - if (terminal.current) { - terminal.current.write(output); - } - } else if (data.type === 'auth_url' && data.url) { - authUrlRef.current = data.url; - setAuthUrl(data.url); - setAuthUrlCopyStatus('idle'); - setIsAuthPanelHidden(false); - } else if (data.type === 'url_open') { - if (data.url) { - authUrlRef.current = data.url; - setAuthUrl(data.url); - setAuthUrlCopyStatus('idle'); - setIsAuthPanelHidden(false); - } - } - } catch (error) { - console.error('[Shell] Error handling WebSocket message:', error, event.data); - } - }; - - ws.current.onclose = (event) => { - setIsConnected(false); - setIsConnecting(false); - setAuthUrlCopyStatus('idle'); - setIsAuthPanelHidden(false); - - if (terminal.current) { - terminal.current.clear(); - terminal.current.write('\x1b[2J\x1b[H'); - } - }; - - ws.current.onerror = (error) => { - setIsConnected(false); - setIsConnecting(false); - }; - } catch (error) { - setIsConnected(false); - setIsConnecting(false); - } - }, [isConnecting, isConnected, openAuthUrlInBrowser]); - - const connectToShell = useCallback(() => { - if (!isInitialized || isConnected || isConnecting) return; - setIsConnecting(true); - connectWebSocket(); - }, [isInitialized, isConnected, isConnecting, connectWebSocket]); - - const disconnectFromShell = useCallback(() => { - if (ws.current) { - ws.current.close(); - ws.current = null; - } - - if (terminal.current) { - terminal.current.clear(); - terminal.current.write('\x1b[2J\x1b[H'); - } - - setIsConnected(false); - setIsConnecting(false); - authUrlRef.current = ''; - setAuthUrl(''); - setAuthUrlCopyStatus('idle'); - setIsAuthPanelHidden(false); - }, []); - - const sessionDisplayName = useMemo(() => { - if (!selectedSession) return null; - return selectedSession.__provider === 'cursor' - ? (selectedSession.name || 'Untitled Session') - : (selectedSession.summary || 'New Session'); - }, [selectedSession]); - - const sessionDisplayNameShort = useMemo(() => { - if (!sessionDisplayName) return null; - return sessionDisplayName.slice(0, 30); - }, [sessionDisplayName]); - - const sessionDisplayNameLong = useMemo(() => { - if (!sessionDisplayName) return null; - return sessionDisplayName.slice(0, 50); - }, [sessionDisplayName]); - - const restartShell = () => { - setIsRestarting(true); - - if (ws.current) { - ws.current.close(); - ws.current = null; - } - - if (terminal.current) { - terminal.current.dispose(); - terminal.current = null; - fitAddon.current = null; - } - - setIsConnected(false); - setIsInitialized(false); - authUrlRef.current = ''; - setAuthUrl(''); - setAuthUrlCopyStatus('idle'); - setIsAuthPanelHidden(false); - - setTimeout(() => { - setIsRestarting(false); - }, 200); - }; - - useEffect(() => { - const currentSessionId = selectedSession?.id || null; - - if (lastSessionId !== null && lastSessionId !== currentSessionId && isInitialized) { - disconnectFromShell(); - } - - setLastSessionId(currentSessionId); - }, [selectedSession?.id, isInitialized, disconnectFromShell]); - - useEffect(() => { - if (!terminalRef.current || !selectedProject || isRestarting || terminal.current) { - return; - } - - - terminal.current = new Terminal({ - cursorBlink: true, - fontSize: 14, - fontFamily: 'Menlo, Monaco, "Courier New", monospace', - allowProposedApi: true, - allowTransparency: false, - convertEol: true, - scrollback: 10000, - tabStopWidth: 4, - windowsMode: false, - macOptionIsMeta: true, - macOptionClickForcesSelection: true, - theme: { - background: '#1e1e1e', - foreground: '#d4d4d4', - cursor: '#ffffff', - cursorAccent: '#1e1e1e', - selection: '#264f78', - selectionForeground: '#ffffff', - black: '#000000', - red: '#cd3131', - green: '#0dbc79', - yellow: '#e5e510', - blue: '#2472c8', - magenta: '#bc3fbc', - cyan: '#11a8cd', - white: '#e5e5e5', - brightBlack: '#666666', - brightRed: '#f14c4c', - brightGreen: '#23d18b', - brightYellow: '#f5f543', - brightBlue: '#3b8eea', - brightMagenta: '#d670d6', - brightCyan: '#29b8db', - brightWhite: '#ffffff', - extendedAnsi: [ - '#000000', '#800000', '#008000', '#808000', - '#000080', '#800080', '#008080', '#c0c0c0', - '#808080', '#ff0000', '#00ff00', '#ffff00', - '#0000ff', '#ff00ff', '#00ffff', '#ffffff' - ] - } - }); - - fitAddon.current = new FitAddon(); - const webglAddon = new WebglAddon(); - const webLinksAddon = new WebLinksAddon(); - - terminal.current.loadAddon(fitAddon.current); - // Disable xterm link auto-detection in minimal (login) mode to avoid partial wrapped URL links. - if (!minimal) { - terminal.current.loadAddon(webLinksAddon); - } - // Note: ClipboardAddon removed - we handle clipboard operations manually in attachCustomKeyEventHandler - - try { - terminal.current.loadAddon(webglAddon); - } catch (error) { - console.warn('[Shell] WebGL renderer unavailable, using Canvas fallback'); - } - - terminal.current.open(terminalRef.current); - - terminal.current.attachCustomKeyEventHandler((event) => { - const activeAuthUrl = isCodexLoginCommand(initialCommandRef.current) - ? CODEX_DEVICE_AUTH_URL - : authUrlRef.current; - - if ( - event.type === 'keydown' && - minimal && - isPlainShellRef.current && - activeAuthUrl && - !event.ctrlKey && - !event.metaKey && - !event.altKey && - event.key?.toLowerCase() === 'c' - ) { - copyAuthUrlToClipboard(activeAuthUrl).catch(() => {}); - } - - if ( - event.type === 'keydown' && - (event.ctrlKey || event.metaKey) && - event.key?.toLowerCase() === 'c' && - terminal.current.hasSelection() - ) { - event.preventDefault(); - event.stopPropagation(); - document.execCommand('copy'); - return false; - } - - if ( - event.type === 'keydown' && - (event.ctrlKey || event.metaKey) && - event.key?.toLowerCase() === 'v' - ) { - // Block native browser/xterm paste so clipboard data is only sent after - // the explicit clipboard-read flow resolves (avoids duplicate pastes). - event.preventDefault(); - event.stopPropagation(); - - navigator.clipboard.readText().then(text => { - if (ws.current && ws.current.readyState === WebSocket.OPEN) { - ws.current.send(JSON.stringify({ - type: 'input', - data: text - })); - } - }).catch(() => {}); - return false; - } - - return true; - }); - - setTimeout(() => { - if (fitAddon.current) { - fitAddon.current.fit(); - if (terminal.current && ws.current && ws.current.readyState === WebSocket.OPEN) { - ws.current.send(JSON.stringify({ - type: 'resize', - cols: terminal.current.cols, - rows: terminal.current.rows - })); - } - } - }, 100); - - setIsInitialized(true); - terminal.current.onData((data) => { - if (ws.current && ws.current.readyState === WebSocket.OPEN) { - ws.current.send(JSON.stringify({ - type: 'input', - data: data - })); - } - }); - - const resizeObserver = new ResizeObserver(() => { - if (fitAddon.current && terminal.current) { - setTimeout(() => { - fitAddon.current.fit(); - if (ws.current && ws.current.readyState === WebSocket.OPEN) { - ws.current.send(JSON.stringify({ - type: 'resize', - cols: terminal.current.cols, - rows: terminal.current.rows - })); - } - }, 50); - } - }); - - if (terminalRef.current) { - resizeObserver.observe(terminalRef.current); - } - - return () => { - resizeObserver.disconnect(); - - if (ws.current && (ws.current.readyState === WebSocket.OPEN || ws.current.readyState === WebSocket.CONNECTING)) { - ws.current.close(); - } - ws.current = null; - - if (terminal.current) { - terminal.current.dispose(); - terminal.current = null; - } - }; - }, [selectedProject?.path || selectedProject?.fullPath, isRestarting, minimal, copyAuthUrlToClipboard]); - - useEffect(() => { - if (!autoConnect || !isInitialized || isConnecting || isConnected) return; - connectToShell(); - }, [autoConnect, isInitialized, isConnecting, isConnected, connectToShell]); - - if (!selectedProject) { - return ( -
-
-
- - - -
-

{t('shell.selectProject.title')}

-

{t('shell.selectProject.description')}

-
-
- ); - } - - if (minimal) { - const displayAuthUrl = isCodexLoginCommand(initialCommand) - ? CODEX_DEVICE_AUTH_URL - : authUrl; - const hasAuthUrl = Boolean(displayAuthUrl); - const showMobileAuthPanel = hasAuthUrl && !isAuthPanelHidden; - const showMobileAuthPanelToggle = hasAuthUrl && isAuthPanelHidden; - - return ( -
-
- {showMobileAuthPanel && ( -
-
-
-

Open or copy the login URL:

- -
- event.currentTarget.select()} - className="w-full rounded border border-gray-600 bg-gray-800 px-2 py-1 text-xs text-gray-100 focus:outline-none focus:ring-1 focus:ring-blue-500" - aria-label="Authentication URL" - /> -
- - -
-
-
- )} - {showMobileAuthPanelToggle && ( -
- -
- )} -
- ); - } - - return ( -
-
-
-
-
- {selectedSession && ( - - ({sessionDisplayNameShort}...) - - )} - {!selectedSession && ( - {t('shell.status.newSession')} - )} - {!isInitialized && ( - {t('shell.status.initializing')} - )} - {isRestarting && ( - {t('shell.status.restarting')} - )} -
-
- {isConnected && ( - - )} - - -
-
-
- -
-
- - {!isInitialized && ( -
-
{t('shell.loading')}
-
- )} - - {isInitialized && !isConnected && !isConnecting && ( -
-
- -

- {isPlainShell ? - t('shell.runCommand', { command: initialCommand || t('shell.defaultCommand'), projectName: selectedProject.displayName }) : - selectedSession ? - t('shell.resumeSession', { displayName: sessionDisplayNameLong }) : - t('shell.startSession') - } -

-
-
- )} - - {isConnecting && ( -
-
-
-
- {t('shell.connecting')} -
-

- {isPlainShell ? - t('shell.runCommand', { command: initialCommand || t('shell.defaultCommand'), projectName: selectedProject.displayName }) : - t('shell.startCli', { projectName: selectedProject.displayName }) - } -

-
-
- )} -
-
- ); -} - -export default Shell; diff --git a/src/components/shell/hooks/useShellConnection.ts b/src/components/shell/hooks/useShellConnection.ts index 997fa40..b60ef94 100644 --- a/src/components/shell/hooks/useShellConnection.ts +++ b/src/components/shell/hooks/useShellConnection.ts @@ -144,9 +144,7 @@ export function useShellConnection({ projectPath: currentProject.fullPath || currentProject.path || '', sessionId: isPlainShellRef.current ? null : selectedSessionRef.current?.id || null, hasSession: isPlainShellRef.current ? false : Boolean(selectedSessionRef.current), - provider: isPlainShellRef.current - ? 'plain-shell' - : selectedSessionRef.current?.__provider || 'claude', + provider: isPlainShellRef.current ? 'plain-shell' : (selectedSessionRef.current?.__provider || localStorage.getItem('selected-provider') || 'claude'), cols: currentTerminal.cols, rows: currentTerminal.rows, initialCommand: initialCommandRef.current,