mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-05 14:07:40 +00:00
* feat: add terminal shortcuts panel for mobile users Slide-out panel providing touch-friendly shortcut buttons (Esc, Tab, Shift+Tab, Arrow Up/Down) and scroll-to-bottom for the terminal. Integrates into the new modular shell architecture by exposing terminalRef and wsRef from useShellRuntime hook and reusing the existing sendSocketMessage utility. Includes localization keys for en, ja, ko, and zh-CN. * fix: replace dual touch/click handlers with unified pointer events Prevents double-fire on touch devices by removing onTouchEnd handlers and using a single onClick for all interactions (mouse, touch, keyboard). onPointerDown with preventDefault handles focus steal prevention. Also clears pending close timer before scheduling a new one to avoid stale timeout overlap. Addresses CodeRabbit review feedback on PR #411. --------- Co-authored-by: Haileyesus <118998054+blackmammoth@users.noreply.github.com>
165 lines
4.4 KiB
TypeScript
165 lines
4.4 KiB
TypeScript
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,
|
|
terminalRef,
|
|
wsRef,
|
|
isConnected,
|
|
isInitialized,
|
|
isConnecting,
|
|
authUrl,
|
|
authUrlVersion,
|
|
connectToShell,
|
|
disconnectFromShell,
|
|
openAuthUrlInBrowser,
|
|
copyAuthUrlToClipboard,
|
|
};
|
|
}
|