mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-03-07 06:57: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>
172 lines
5.3 KiB
TypeScript
172 lines
5.3 KiB
TypeScript
import { useCallback, useMemo, useState } from 'react';
|
|
import { useTranslation } from 'react-i18next';
|
|
import '@xterm/xterm/css/xterm.css';
|
|
import type { Project, ProjectSession } from '../../../types/app';
|
|
import { SHELL_RESTART_DELAY_MS } from '../constants/constants';
|
|
import { useShellRuntime } from '../hooks/useShellRuntime';
|
|
import { getSessionDisplayName } from '../utils/auth';
|
|
import ShellConnectionOverlay from './subcomponents/ShellConnectionOverlay';
|
|
import ShellEmptyState from './subcomponents/ShellEmptyState';
|
|
import ShellHeader from './subcomponents/ShellHeader';
|
|
import ShellMinimalView from './subcomponents/ShellMinimalView';
|
|
import TerminalShortcutsPanel from './subcomponents/TerminalShortcutsPanel';
|
|
|
|
type ShellProps = {
|
|
selectedProject?: Project | null;
|
|
selectedSession?: ProjectSession | null;
|
|
initialCommand?: string | null;
|
|
isPlainShell?: boolean;
|
|
onProcessComplete?: ((exitCode: number) => void) | null;
|
|
minimal?: boolean;
|
|
autoConnect?: boolean;
|
|
isActive?: boolean;
|
|
};
|
|
|
|
export default function Shell({
|
|
selectedProject = null,
|
|
selectedSession = null,
|
|
initialCommand = null,
|
|
isPlainShell = false,
|
|
onProcessComplete = null,
|
|
minimal = false,
|
|
autoConnect = false,
|
|
isActive,
|
|
}: ShellProps) {
|
|
const { t } = useTranslation('chat');
|
|
const [isRestarting, setIsRestarting] = useState(false);
|
|
|
|
// Keep the public API stable for existing callers that still pass `isActive`.
|
|
void isActive;
|
|
|
|
const {
|
|
terminalContainerRef,
|
|
terminalRef,
|
|
wsRef,
|
|
isConnected,
|
|
isInitialized,
|
|
isConnecting,
|
|
authUrl,
|
|
authUrlVersion,
|
|
connectToShell,
|
|
disconnectFromShell,
|
|
openAuthUrlInBrowser,
|
|
copyAuthUrlToClipboard,
|
|
} = useShellRuntime({
|
|
selectedProject,
|
|
selectedSession,
|
|
initialCommand,
|
|
isPlainShell,
|
|
minimal,
|
|
autoConnect,
|
|
isRestarting,
|
|
onProcessComplete,
|
|
});
|
|
|
|
const sessionDisplayName = useMemo(() => getSessionDisplayName(selectedSession), [selectedSession]);
|
|
const sessionDisplayNameShort = useMemo(
|
|
() => (sessionDisplayName ? sessionDisplayName.slice(0, 30) : null),
|
|
[sessionDisplayName],
|
|
);
|
|
const sessionDisplayNameLong = useMemo(
|
|
() => (sessionDisplayName ? sessionDisplayName.slice(0, 50) : null),
|
|
[sessionDisplayName],
|
|
);
|
|
|
|
const handleRestartShell = useCallback(() => {
|
|
setIsRestarting(true);
|
|
window.setTimeout(() => {
|
|
setIsRestarting(false);
|
|
}, SHELL_RESTART_DELAY_MS);
|
|
}, []);
|
|
|
|
if (!selectedProject) {
|
|
return (
|
|
<ShellEmptyState
|
|
title={t('shell.selectProject.title')}
|
|
description={t('shell.selectProject.description')}
|
|
/>
|
|
);
|
|
}
|
|
|
|
if (minimal) {
|
|
return (
|
|
<ShellMinimalView
|
|
terminalContainerRef={terminalContainerRef}
|
|
authUrl={authUrl}
|
|
authUrlVersion={authUrlVersion}
|
|
initialCommand={initialCommand}
|
|
isConnected={isConnected}
|
|
openAuthUrlInBrowser={openAuthUrlInBrowser}
|
|
copyAuthUrlToClipboard={copyAuthUrlToClipboard}
|
|
/>
|
|
);
|
|
}
|
|
|
|
const readyDescription = isPlainShell
|
|
? t('shell.runCommand', {
|
|
command: initialCommand || t('shell.defaultCommand'),
|
|
projectName: selectedProject.displayName,
|
|
})
|
|
: selectedSession
|
|
? t('shell.resumeSession', { displayName: sessionDisplayNameLong })
|
|
: t('shell.startSession');
|
|
|
|
const connectingDescription = isPlainShell
|
|
? t('shell.runCommand', {
|
|
command: initialCommand || t('shell.defaultCommand'),
|
|
projectName: selectedProject.displayName,
|
|
})
|
|
: t('shell.startCli', { projectName: selectedProject.displayName });
|
|
|
|
const overlayMode = !isInitialized ? 'loading' : isConnecting ? 'connecting' : !isConnected ? 'connect' : null;
|
|
const overlayDescription = overlayMode === 'connecting' ? connectingDescription : readyDescription;
|
|
|
|
return (
|
|
<div className="h-full flex flex-col bg-gray-900 w-full">
|
|
<ShellHeader
|
|
isConnected={isConnected}
|
|
isInitialized={isInitialized}
|
|
isRestarting={isRestarting}
|
|
hasSession={Boolean(selectedSession)}
|
|
sessionDisplayNameShort={sessionDisplayNameShort}
|
|
onDisconnect={disconnectFromShell}
|
|
onRestart={handleRestartShell}
|
|
statusNewSessionText={t('shell.status.newSession')}
|
|
statusInitializingText={t('shell.status.initializing')}
|
|
statusRestartingText={t('shell.status.restarting')}
|
|
disconnectLabel={t('shell.actions.disconnect')}
|
|
disconnectTitle={t('shell.actions.disconnectTitle')}
|
|
restartLabel={t('shell.actions.restart')}
|
|
restartTitle={t('shell.actions.restartTitle')}
|
|
disableRestart={isRestarting || isConnected}
|
|
/>
|
|
|
|
<div className="flex-1 p-2 overflow-hidden relative">
|
|
<div
|
|
ref={terminalContainerRef}
|
|
className="h-full w-full focus:outline-none"
|
|
style={{ outline: 'none' }}
|
|
/>
|
|
|
|
{overlayMode && (
|
|
<ShellConnectionOverlay
|
|
mode={overlayMode}
|
|
description={overlayDescription}
|
|
loadingLabel={t('shell.loading')}
|
|
connectLabel={t('shell.actions.connect')}
|
|
connectTitle={t('shell.actions.connectTitle')}
|
|
connectingLabel={t('shell.connecting')}
|
|
onConnect={connectToShell}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
<TerminalShortcutsPanel
|
|
wsRef={wsRef}
|
|
terminalRef={terminalRef}
|
|
isConnected={isConnected}
|
|
/>
|
|
</div>
|
|
);
|
|
}
|