mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-04-11 16:11:31 +00:00
* fix: update tooltip component * fix: remove the mobile navigation component In addition, - the sidebar is also updated to take full space - the terminal shortcuts in shell are updated to not interfere with the shell content. * fix: remove mobile nav component * fix: remove "Thinking..." indicator In addition, the claude status component has been restyled to be more compact and less obtrusive. - The type and prop arguments for ChatMessagesPane have been updated to remove the isLoading prop, which was only used to control the display of the AssistantThinkingIndicator. * fix: show elapsed time only when loading --------- Co-authored-by: Haileyesus <something@gmail.com> Co-authored-by: Simos Mikelatos <simosmik@gmail.com>
265 lines
10 KiB
TypeScript
265 lines
10 KiB
TypeScript
import { useTranslation } from 'react-i18next';
|
|
import { useCallback, useRef } from 'react';
|
|
import type { Dispatch, RefObject, SetStateAction } from 'react';
|
|
import type { ChatMessage } from '../../types/types';
|
|
import type { Project, ProjectSession, SessionProvider } from '../../../../types/app';
|
|
import { getIntrinsicMessageKey } from '../../utils/messageKeys';
|
|
import MessageComponent from './MessageComponent';
|
|
import ProviderSelectionEmptyState from './ProviderSelectionEmptyState';
|
|
|
|
interface ChatMessagesPaneProps {
|
|
scrollContainerRef: RefObject<HTMLDivElement>;
|
|
onWheel: () => void;
|
|
onTouchMove: () => void;
|
|
isLoadingSessionMessages: boolean;
|
|
chatMessages: ChatMessage[];
|
|
selectedSession: ProjectSession | null;
|
|
currentSessionId: string | null;
|
|
provider: SessionProvider;
|
|
setProvider: (provider: SessionProvider) => void;
|
|
textareaRef: RefObject<HTMLTextAreaElement>;
|
|
claudeModel: string;
|
|
setClaudeModel: (model: string) => void;
|
|
cursorModel: string;
|
|
setCursorModel: (model: string) => void;
|
|
codexModel: string;
|
|
setCodexModel: (model: string) => void;
|
|
geminiModel: string;
|
|
setGeminiModel: (model: string) => void;
|
|
tasksEnabled: boolean;
|
|
isTaskMasterInstalled: boolean | null;
|
|
onShowAllTasks?: (() => void) | null;
|
|
setInput: Dispatch<SetStateAction<string>>;
|
|
isLoadingMoreMessages: boolean;
|
|
hasMoreMessages: boolean;
|
|
totalMessages: number;
|
|
sessionMessagesCount: number;
|
|
visibleMessageCount: number;
|
|
visibleMessages: ChatMessage[];
|
|
loadEarlierMessages: () => void;
|
|
loadAllMessages: () => void;
|
|
allMessagesLoaded: boolean;
|
|
isLoadingAllMessages: boolean;
|
|
loadAllJustFinished: boolean;
|
|
showLoadAllOverlay: boolean;
|
|
createDiff: any;
|
|
onFileOpen?: (filePath: string, diffInfo?: unknown) => void;
|
|
onShowSettings?: () => void;
|
|
onGrantToolPermission: (suggestion: { entry: string; toolName: string }) => { success: boolean };
|
|
autoExpandTools?: boolean;
|
|
showRawParameters?: boolean;
|
|
showThinking?: boolean;
|
|
selectedProject: Project;
|
|
}
|
|
|
|
export default function ChatMessagesPane({
|
|
scrollContainerRef,
|
|
onWheel,
|
|
onTouchMove,
|
|
isLoadingSessionMessages,
|
|
chatMessages,
|
|
selectedSession,
|
|
currentSessionId,
|
|
provider,
|
|
setProvider,
|
|
textareaRef,
|
|
claudeModel,
|
|
setClaudeModel,
|
|
cursorModel,
|
|
setCursorModel,
|
|
codexModel,
|
|
setCodexModel,
|
|
geminiModel,
|
|
setGeminiModel,
|
|
tasksEnabled,
|
|
isTaskMasterInstalled,
|
|
onShowAllTasks,
|
|
setInput,
|
|
isLoadingMoreMessages,
|
|
hasMoreMessages,
|
|
totalMessages,
|
|
sessionMessagesCount,
|
|
visibleMessageCount,
|
|
visibleMessages,
|
|
loadEarlierMessages,
|
|
loadAllMessages,
|
|
allMessagesLoaded,
|
|
isLoadingAllMessages,
|
|
loadAllJustFinished,
|
|
showLoadAllOverlay,
|
|
createDiff,
|
|
onFileOpen,
|
|
onShowSettings,
|
|
onGrantToolPermission,
|
|
autoExpandTools,
|
|
showRawParameters,
|
|
showThinking,
|
|
selectedProject,
|
|
}: ChatMessagesPaneProps) {
|
|
const { t } = useTranslation('chat');
|
|
const messageKeyMapRef = useRef<WeakMap<ChatMessage, string>>(new WeakMap());
|
|
const allocatedKeysRef = useRef<Set<string>>(new Set());
|
|
const generatedMessageKeyCounterRef = useRef(0);
|
|
|
|
// Keep keys stable across prepends so existing MessageComponent instances retain local state.
|
|
const getMessageKey = useCallback((message: ChatMessage) => {
|
|
const existingKey = messageKeyMapRef.current.get(message);
|
|
if (existingKey) {
|
|
return existingKey;
|
|
}
|
|
|
|
const intrinsicKey = getIntrinsicMessageKey(message);
|
|
let candidateKey = intrinsicKey;
|
|
|
|
if (!candidateKey || allocatedKeysRef.current.has(candidateKey)) {
|
|
do {
|
|
generatedMessageKeyCounterRef.current += 1;
|
|
candidateKey = intrinsicKey
|
|
? `${intrinsicKey}-${generatedMessageKeyCounterRef.current}`
|
|
: `message-generated-${generatedMessageKeyCounterRef.current}`;
|
|
} while (allocatedKeysRef.current.has(candidateKey));
|
|
}
|
|
|
|
allocatedKeysRef.current.add(candidateKey);
|
|
messageKeyMapRef.current.set(message, candidateKey);
|
|
return candidateKey;
|
|
}, []);
|
|
|
|
return (
|
|
<div
|
|
ref={scrollContainerRef}
|
|
onWheel={onWheel}
|
|
onTouchMove={onTouchMove}
|
|
className="relative flex-1 space-y-3 overflow-y-auto overflow-x-hidden px-0 py-3 sm:space-y-4 sm:p-4"
|
|
>
|
|
{isLoadingSessionMessages && chatMessages.length === 0 ? (
|
|
<div className="mt-8 text-center text-gray-500 dark:text-gray-400">
|
|
<div className="flex items-center justify-center space-x-2">
|
|
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-gray-400" />
|
|
<p>{t('session.loading.sessionMessages')}</p>
|
|
</div>
|
|
</div>
|
|
) : chatMessages.length === 0 ? (
|
|
<ProviderSelectionEmptyState
|
|
selectedSession={selectedSession}
|
|
currentSessionId={currentSessionId}
|
|
provider={provider}
|
|
setProvider={setProvider}
|
|
textareaRef={textareaRef}
|
|
claudeModel={claudeModel}
|
|
setClaudeModel={setClaudeModel}
|
|
cursorModel={cursorModel}
|
|
setCursorModel={setCursorModel}
|
|
codexModel={codexModel}
|
|
setCodexModel={setCodexModel}
|
|
geminiModel={geminiModel}
|
|
setGeminiModel={setGeminiModel}
|
|
tasksEnabled={tasksEnabled}
|
|
isTaskMasterInstalled={isTaskMasterInstalled}
|
|
onShowAllTasks={onShowAllTasks}
|
|
setInput={setInput}
|
|
/>
|
|
) : (
|
|
<>
|
|
{/* Loading indicator for older messages (hide when load-all is active) */}
|
|
{isLoadingMoreMessages && !isLoadingAllMessages && !allMessagesLoaded && (
|
|
<div className="py-3 text-center text-gray-500 dark:text-gray-400">
|
|
<div className="flex items-center justify-center space-x-2">
|
|
<div className="h-4 w-4 animate-spin rounded-full border-b-2 border-gray-400" />
|
|
<p className="text-sm">{t('session.loading.olderMessages')}</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
{/* Indicator showing there are more messages to load (hide when all loaded) */}
|
|
{hasMoreMessages && !isLoadingMoreMessages && !allMessagesLoaded && (
|
|
<div className="border-b border-gray-200 py-2 text-center text-sm text-gray-500 dark:border-gray-700 dark:text-gray-400">
|
|
{totalMessages > 0 && (
|
|
<span>
|
|
{t('session.messages.showingOf', { shown: sessionMessagesCount, total: totalMessages })}{' '}
|
|
<span className="text-xs">{t('session.messages.scrollToLoad')}</span>
|
|
</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Floating "Load all messages" overlay */}
|
|
{(showLoadAllOverlay || isLoadingAllMessages || loadAllJustFinished) && (
|
|
<div className="pointer-events-none sticky top-2 z-20 flex justify-center">
|
|
{loadAllJustFinished ? (
|
|
<div className="flex items-center space-x-2 rounded-full bg-green-600 px-4 py-1.5 text-xs font-medium text-white shadow-lg dark:bg-green-500">
|
|
<svg className="h-3 w-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={3} d="M5 13l4 4L19 7" />
|
|
</svg>
|
|
<span>{t('session.messages.allLoaded')}</span>
|
|
</div>
|
|
) : (
|
|
<button
|
|
className="pointer-events-auto flex items-center space-x-2 rounded-full bg-blue-600 px-4 py-1.5 text-xs font-medium text-white shadow-lg transition-all duration-200 hover:scale-105 hover:bg-blue-700 disabled:cursor-wait disabled:opacity-75 dark:bg-blue-500 dark:hover:bg-blue-600"
|
|
onClick={loadAllMessages}
|
|
disabled={isLoadingAllMessages}
|
|
>
|
|
{isLoadingAllMessages && (
|
|
<div className="h-3 w-3 animate-spin rounded-full border-2 border-white/30 border-t-white" />
|
|
)}
|
|
<span>
|
|
{isLoadingAllMessages
|
|
? t('session.messages.loadingAll')
|
|
: <>{t('session.messages.loadAll')} {totalMessages > 0 && `(${totalMessages})`}</>
|
|
}
|
|
</span>
|
|
</button>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Performance warning when all messages are loaded */}
|
|
{allMessagesLoaded && (
|
|
<div className="border-b border-amber-200 bg-amber-50 py-1.5 text-center text-xs text-amber-600 dark:border-amber-800 dark:bg-amber-900/20 dark:text-amber-400">
|
|
{t('session.messages.perfWarning')}
|
|
</div>
|
|
)}
|
|
|
|
{/* Legacy message count indicator (for non-paginated view) */}
|
|
{!hasMoreMessages && chatMessages.length > visibleMessageCount && (
|
|
<div className="border-b border-gray-200 py-2 text-center text-sm text-gray-500 dark:border-gray-700 dark:text-gray-400">
|
|
{t('session.messages.showingLast', { count: visibleMessageCount, total: chatMessages.length })} |
|
|
<button className="ml-1 text-blue-600 underline hover:text-blue-700" onClick={loadEarlierMessages}>
|
|
{t('session.messages.loadEarlier')}
|
|
</button>
|
|
{' | '}
|
|
<button
|
|
className="text-blue-600 underline hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
|
onClick={loadAllMessages}
|
|
>
|
|
{t('session.messages.loadAll')}
|
|
</button>
|
|
</div>
|
|
)}
|
|
|
|
{visibleMessages.map((message, index) => {
|
|
const prevMessage = index > 0 ? visibleMessages[index - 1] : null;
|
|
return (
|
|
<MessageComponent
|
|
key={getMessageKey(message)}
|
|
message={message}
|
|
prevMessage={prevMessage}
|
|
createDiff={createDiff}
|
|
onFileOpen={onFileOpen}
|
|
onShowSettings={onShowSettings}
|
|
onGrantToolPermission={onGrantToolPermission}
|
|
autoExpandTools={autoExpandTools}
|
|
showRawParameters={showRawParameters}
|
|
showThinking={showThinking}
|
|
selectedProject={selectedProject}
|
|
provider={provider}
|
|
/>
|
|
);
|
|
})}
|
|
</>
|
|
)}
|
|
</div>
|
|
);
|
|
}
|
|
|