Refactor Settings, FileTree, GitPanel, Shell, and CodeEditor components (#402)

This commit is contained in:
Haileyesus
2026-02-25 19:07:07 +03:00
committed by GitHub
parent 23801e9cc1
commit 5e3a7b69d7
149 changed files with 11627 additions and 8453 deletions

View File

@@ -0,0 +1,229 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { MutableRefObject } from 'react';
import type { FitAddon } from '@xterm/addon-fit';
import type { Terminal } from '@xterm/xterm';
import type { Project, ProjectSession } from '../../../types/app';
import { TERMINAL_INIT_DELAY_MS } from '../constants/constants';
import { getShellWebSocketUrl, parseShellMessage, sendSocketMessage } from '../utils/socket';
const ANSI_ESCAPE_REGEX =
/(?:\u001B\[[0-?]*[ -/]*[@-~]|\u009B[0-?]*[ -/]*[@-~]|\u001B\][^\u0007\u001B]*(?:\u0007|\u001B\\)|\u009D[^\u0007\u009C]*(?:\u0007|\u009C)|\u001B[PX^_][^\u001B]*\u001B\\|[\u0090\u0098\u009E\u009F][^\u009C]*\u009C|\u001B[@-Z\\-_])/g;
const PROCESS_EXIT_REGEX = /Process exited with code (\d+)/;
type UseShellConnectionOptions = {
wsRef: MutableRefObject<WebSocket | null>;
terminalRef: MutableRefObject<Terminal | null>;
fitAddonRef: MutableRefObject<FitAddon | null>;
selectedProjectRef: MutableRefObject<Project | null | undefined>;
selectedSessionRef: MutableRefObject<ProjectSession | null | undefined>;
initialCommandRef: MutableRefObject<string | null | undefined>;
isPlainShellRef: MutableRefObject<boolean>;
onProcessCompleteRef: MutableRefObject<((exitCode: number) => void) | null | undefined>;
isInitialized: boolean;
autoConnect: boolean;
closeSocket: () => void;
clearTerminalScreen: () => void;
setAuthUrl: (nextAuthUrl: string) => void;
};
type UseShellConnectionResult = {
isConnected: boolean;
isConnecting: boolean;
closeSocket: () => void;
connectToShell: () => void;
disconnectFromShell: () => void;
};
export function useShellConnection({
wsRef,
terminalRef,
fitAddonRef,
selectedProjectRef,
selectedSessionRef,
initialCommandRef,
isPlainShellRef,
onProcessCompleteRef,
isInitialized,
autoConnect,
closeSocket,
clearTerminalScreen,
setAuthUrl,
}: UseShellConnectionOptions): UseShellConnectionResult {
const [isConnected, setIsConnected] = useState(false);
const [isConnecting, setIsConnecting] = useState(false);
const connectingRef = useRef(false);
const handleProcessCompletion = useCallback(
(output: string) => {
if (!isPlainShellRef.current || !onProcessCompleteRef.current) {
return;
}
const sanitizedOutput = output.replace(ANSI_ESCAPE_REGEX, '');
const cleanOutput = sanitizedOutput;
if (cleanOutput.includes('Process exited with code 0')) {
onProcessCompleteRef.current(0);
return;
}
const match = cleanOutput.match(PROCESS_EXIT_REGEX);
if (!match) {
return;
}
const exitCode = Number.parseInt(match[1], 10);
if (!Number.isNaN(exitCode) && exitCode !== 0) {
onProcessCompleteRef.current(exitCode);
}
},
[isPlainShellRef, onProcessCompleteRef],
);
const handleSocketMessage = useCallback(
(rawPayload: string) => {
const message = parseShellMessage(rawPayload);
if (!message) {
console.error('[Shell] Error handling WebSocket message:', rawPayload);
return;
}
if (message.type === 'output') {
const output = typeof message.data === 'string' ? message.data : '';
handleProcessCompletion(output);
terminalRef.current?.write(output);
return;
}
if (message.type === 'auth_url' || message.type === 'url_open') {
const nextAuthUrl = typeof message.url === 'string' ? message.url : '';
if (nextAuthUrl) {
setAuthUrl(nextAuthUrl);
}
}
},
[handleProcessCompletion, setAuthUrl, terminalRef],
);
const connectWebSocket = useCallback(
(isConnectionLocked = false) => {
if ((connectingRef.current && !isConnectionLocked) || isConnecting || isConnected) {
return;
}
try {
const wsUrl = getShellWebSocketUrl();
if (!wsUrl) {
connectingRef.current = false;
setIsConnecting(false);
return;
}
connectingRef.current = true;
const socket = new WebSocket(wsUrl);
wsRef.current = socket;
socket.onopen = () => {
setIsConnected(true);
setIsConnecting(false);
connectingRef.current = false;
setAuthUrl('');
window.setTimeout(() => {
const currentTerminal = terminalRef.current;
const currentFitAddon = fitAddonRef.current;
const currentProject = selectedProjectRef.current;
if (!currentTerminal || !currentFitAddon || !currentProject) {
return;
}
currentFitAddon.fit();
sendSocketMessage(socket, {
type: 'init',
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 || localStorage.getItem('selected-provider') || 'claude'),
cols: currentTerminal.cols,
rows: currentTerminal.rows,
initialCommand: initialCommandRef.current,
isPlainShell: isPlainShellRef.current,
});
}, TERMINAL_INIT_DELAY_MS);
};
socket.onmessage = (event) => {
const rawPayload = typeof event.data === 'string' ? event.data : String(event.data ?? '');
handleSocketMessage(rawPayload);
};
socket.onclose = () => {
setIsConnected(false);
setIsConnecting(false);
connectingRef.current = false;
clearTerminalScreen();
};
socket.onerror = () => {
setIsConnected(false);
setIsConnecting(false);
connectingRef.current = false;
};
} catch {
setIsConnected(false);
setIsConnecting(false);
connectingRef.current = false;
}
},
[
clearTerminalScreen,
fitAddonRef,
handleSocketMessage,
initialCommandRef,
isConnected,
isConnecting,
isPlainShellRef,
selectedProjectRef,
selectedSessionRef,
setAuthUrl,
terminalRef,
wsRef,
],
);
const connectToShell = useCallback(() => {
if (!isInitialized || isConnected || isConnecting || connectingRef.current) {
return;
}
connectingRef.current = true;
setIsConnecting(true);
connectWebSocket(true);
}, [connectWebSocket, isConnected, isConnecting, isInitialized]);
const disconnectFromShell = useCallback(() => {
closeSocket();
clearTerminalScreen();
setIsConnected(false);
setIsConnecting(false);
connectingRef.current = false;
setAuthUrl('');
}, [clearTerminalScreen, closeSocket, setAuthUrl]);
useEffect(() => {
if (!autoConnect || !isInitialized || isConnecting || isConnected) {
return;
}
connectToShell();
}, [autoConnect, connectToShell, isConnected, isConnecting, isInitialized]);
return {
isConnected,
isConnecting,
closeSocket,
connectToShell,
disconnectFromShell,
};
}

View File

@@ -0,0 +1,162 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { FitAddon } from '@xterm/addon-fit';
import type { Terminal } from '@xterm/xterm';
import { useShellConnection } from './useShellConnection';
import { useShellTerminal } from './useShellTerminal';
import type { UseShellRuntimeOptions, UseShellRuntimeResult } from '../types/types';
import { copyTextToClipboard } from '../../../utils/clipboard';
export function useShellRuntime({
selectedProject,
selectedSession,
initialCommand,
isPlainShell,
minimal,
autoConnect,
isRestarting,
onProcessComplete,
}: UseShellRuntimeOptions): UseShellRuntimeResult {
const terminalContainerRef = useRef<HTMLDivElement>(null);
const terminalRef = useRef<Terminal | null>(null);
const fitAddonRef = useRef<FitAddon | null>(null);
const wsRef = useRef<WebSocket | null>(null);
const [authUrl, setAuthUrl] = useState('');
const [authUrlVersion, setAuthUrlVersion] = useState(0);
const selectedProjectRef = useRef(selectedProject);
const selectedSessionRef = useRef(selectedSession);
const initialCommandRef = useRef(initialCommand);
const isPlainShellRef = useRef(isPlainShell);
const onProcessCompleteRef = useRef(onProcessComplete);
const authUrlRef = useRef('');
const lastSessionIdRef = useRef<string | null>(selectedSession?.id ?? null);
// Keep mutable values in refs so websocket handlers always read current data.
useEffect(() => {
selectedProjectRef.current = selectedProject;
selectedSessionRef.current = selectedSession;
initialCommandRef.current = initialCommand;
isPlainShellRef.current = isPlainShell;
onProcessCompleteRef.current = onProcessComplete;
}, [selectedProject, selectedSession, initialCommand, isPlainShell, onProcessComplete]);
const setCurrentAuthUrl = useCallback((nextAuthUrl: string) => {
authUrlRef.current = nextAuthUrl;
setAuthUrl(nextAuthUrl);
setAuthUrlVersion((previous) => previous + 1);
}, []);
const closeSocket = useCallback(() => {
const activeSocket = wsRef.current;
if (!activeSocket) {
return;
}
if (
activeSocket.readyState === WebSocket.OPEN ||
activeSocket.readyState === WebSocket.CONNECTING
) {
activeSocket.close();
}
wsRef.current = null;
}, []);
const openAuthUrlInBrowser = useCallback((url = authUrlRef.current) => {
if (!url) {
return false;
}
const popup = window.open(url, '_blank');
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;
}
return copyTextToClipboard(url);
}, []);
const { isInitialized, clearTerminalScreen, disposeTerminal } = useShellTerminal({
terminalContainerRef,
terminalRef,
fitAddonRef,
wsRef,
selectedProject,
minimal,
isRestarting,
initialCommandRef,
isPlainShellRef,
authUrlRef,
copyAuthUrlToClipboard,
closeSocket,
});
const { isConnected, isConnecting, connectToShell, disconnectFromShell } = useShellConnection({
wsRef,
terminalRef,
fitAddonRef,
selectedProjectRef,
selectedSessionRef,
initialCommandRef,
isPlainShellRef,
onProcessCompleteRef,
isInitialized,
autoConnect,
closeSocket,
clearTerminalScreen,
setAuthUrl: setCurrentAuthUrl,
});
useEffect(() => {
if (!isRestarting) {
return;
}
disconnectFromShell();
disposeTerminal();
}, [disconnectFromShell, disposeTerminal, isRestarting]);
useEffect(() => {
if (selectedProject) {
return;
}
disconnectFromShell();
disposeTerminal();
}, [disconnectFromShell, disposeTerminal, selectedProject]);
useEffect(() => {
const currentSessionId = selectedSession?.id ?? null;
if (lastSessionIdRef.current !== currentSessionId && isInitialized) {
disconnectFromShell();
}
lastSessionIdRef.current = currentSessionId;
}, [disconnectFromShell, isInitialized, selectedSession?.id]);
return {
terminalContainerRef,
isConnected,
isInitialized,
isConnecting,
authUrl,
authUrlVersion,
connectToShell,
disconnectFromShell,
openAuthUrlInBrowser,
copyAuthUrlToClipboard,
};
}

View File

@@ -0,0 +1,245 @@
import { useCallback, useEffect, useRef, useState } from 'react';
import type { MutableRefObject, RefObject } from 'react';
import { FitAddon } from '@xterm/addon-fit';
import { WebLinksAddon } from '@xterm/addon-web-links';
import { WebglAddon } from '@xterm/addon-webgl';
import { Terminal } from '@xterm/xterm';
import type { Project } from '../../../types/app';
import {
CODEX_DEVICE_AUTH_URL,
TERMINAL_INIT_DELAY_MS,
TERMINAL_OPTIONS,
TERMINAL_RESIZE_DELAY_MS,
} from '../constants/constants';
import { isCodexLoginCommand } from '../utils/auth';
import { sendSocketMessage } from '../utils/socket';
import { ensureXtermFocusStyles } from '../utils/terminalStyles';
type UseShellTerminalOptions = {
terminalContainerRef: RefObject<HTMLDivElement>;
terminalRef: MutableRefObject<Terminal | null>;
fitAddonRef: MutableRefObject<FitAddon | null>;
wsRef: MutableRefObject<WebSocket | null>;
selectedProject: Project | null | undefined;
minimal: boolean;
isRestarting: boolean;
initialCommandRef: MutableRefObject<string | null | undefined>;
isPlainShellRef: MutableRefObject<boolean>;
authUrlRef: MutableRefObject<string>;
copyAuthUrlToClipboard: (url?: string) => Promise<boolean>;
closeSocket: () => void;
};
type UseShellTerminalResult = {
isInitialized: boolean;
clearTerminalScreen: () => void;
disposeTerminal: () => void;
};
export function useShellTerminal({
terminalContainerRef,
terminalRef,
fitAddonRef,
wsRef,
selectedProject,
minimal,
isRestarting,
initialCommandRef,
isPlainShellRef,
authUrlRef,
copyAuthUrlToClipboard,
closeSocket,
}: UseShellTerminalOptions): UseShellTerminalResult {
const [isInitialized, setIsInitialized] = useState(false);
const resizeTimeoutRef = useRef<number | null>(null);
const selectedProjectKey = selectedProject?.fullPath || selectedProject?.path || '';
const hasSelectedProject = Boolean(selectedProject);
useEffect(() => {
ensureXtermFocusStyles();
}, []);
const clearTerminalScreen = useCallback(() => {
if (!terminalRef.current) {
return;
}
terminalRef.current.clear();
terminalRef.current.write('\x1b[2J\x1b[H');
}, [terminalRef]);
const disposeTerminal = useCallback(() => {
if (terminalRef.current) {
terminalRef.current.dispose();
terminalRef.current = null;
}
fitAddonRef.current = null;
setIsInitialized(false);
}, [fitAddonRef, terminalRef]);
useEffect(() => {
if (!terminalContainerRef.current || !hasSelectedProject || isRestarting || terminalRef.current) {
return;
}
const nextTerminal = new Terminal(TERMINAL_OPTIONS);
terminalRef.current = nextTerminal;
const nextFitAddon = new FitAddon();
fitAddonRef.current = nextFitAddon;
nextTerminal.loadAddon(nextFitAddon);
// Avoid wrapped partial links in compact login flows.
if (!minimal) {
nextTerminal.loadAddon(new WebLinksAddon());
}
try {
nextTerminal.loadAddon(new WebglAddon());
} catch {
console.warn('[Shell] WebGL renderer unavailable, using Canvas fallback');
}
nextTerminal.open(terminalContainerRef.current);
nextTerminal.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'
) {
event.preventDefault();
event.stopPropagation();
void copyAuthUrlToClipboard(activeAuthUrl);
return false;
}
if (
event.type === 'keydown' &&
(event.ctrlKey || event.metaKey) &&
event.key?.toLowerCase() === 'c' &&
nextTerminal.hasSelection()
) {
event.preventDefault();
event.stopPropagation();
document.execCommand('copy');
return false;
}
if (
event.type === 'keydown' &&
(event.ctrlKey || event.metaKey) &&
event.key?.toLowerCase() === 'v'
) {
// Block native paste so data is only injected after clipboard-read resolves.
event.preventDefault();
event.stopPropagation();
if (typeof navigator !== 'undefined' && navigator.clipboard?.readText) {
navigator.clipboard
.readText()
.then((text) => {
sendSocketMessage(wsRef.current, {
type: 'input',
data: text,
});
})
.catch(() => {});
}
return false;
}
return true;
});
window.setTimeout(() => {
const currentFitAddon = fitAddonRef.current;
const currentTerminal = terminalRef.current;
if (!currentFitAddon || !currentTerminal) {
return;
}
currentFitAddon.fit();
sendSocketMessage(wsRef.current, {
type: 'resize',
cols: currentTerminal.cols,
rows: currentTerminal.rows,
});
}, TERMINAL_INIT_DELAY_MS);
setIsInitialized(true);
const dataSubscription = nextTerminal.onData((data) => {
sendSocketMessage(wsRef.current, {
type: 'input',
data,
});
});
const resizeObserver = new ResizeObserver(() => {
if (resizeTimeoutRef.current !== null) {
window.clearTimeout(resizeTimeoutRef.current);
}
resizeTimeoutRef.current = window.setTimeout(() => {
const currentFitAddon = fitAddonRef.current;
const currentTerminal = terminalRef.current;
if (!currentFitAddon || !currentTerminal) {
return;
}
currentFitAddon.fit();
sendSocketMessage(wsRef.current, {
type: 'resize',
cols: currentTerminal.cols,
rows: currentTerminal.rows,
});
}, TERMINAL_RESIZE_DELAY_MS);
});
resizeObserver.observe(terminalContainerRef.current);
return () => {
resizeObserver.disconnect();
if (resizeTimeoutRef.current !== null) {
window.clearTimeout(resizeTimeoutRef.current);
resizeTimeoutRef.current = null;
}
dataSubscription.dispose();
closeSocket();
disposeTerminal();
};
}, [
authUrlRef,
closeSocket,
copyAuthUrlToClipboard,
disposeTerminal,
fitAddonRef,
initialCommandRef,
isPlainShellRef,
isRestarting,
minimal,
hasSelectedProject,
selectedProjectKey,
terminalContainerRef,
terminalRef,
wsRef,
]);
return {
isInitialized,
clearTerminalScreen,
disposeTerminal,
};
}