mirror of
https://github.com/siteboon/claudecodeui.git
synced 2026-06-27 06:05:54 +08:00
fix: improve desktop chat performance
This commit is contained in:
@@ -204,6 +204,8 @@ export function useChatComposerState({
|
||||
|
||||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||||
const inputHighlightRef = useRef<HTMLDivElement>(null);
|
||||
const textareaLineHeightRef = useRef<number | null>(null);
|
||||
const lastAutosizedInputRef = useRef<string | null>(null);
|
||||
const handleSubmitRef = useRef<
|
||||
((event: FormEvent<HTMLFormElement> | MouseEvent | TouchEvent | KeyboardEvent<HTMLTextAreaElement>) => Promise<void>) | null
|
||||
>(null);
|
||||
@@ -457,6 +459,22 @@ export function useChatComposerState({
|
||||
inputHighlightRef.current.scrollLeft = target.scrollLeft;
|
||||
}, []);
|
||||
|
||||
const resizeTextarea = useCallback((target: HTMLTextAreaElement) => {
|
||||
target.style.height = 'auto';
|
||||
const nextHeight = Math.max(22, target.scrollHeight);
|
||||
target.style.height = `${nextHeight}px`;
|
||||
|
||||
let lineHeight = textareaLineHeightRef.current;
|
||||
if (!lineHeight) {
|
||||
lineHeight = parseInt(window.getComputedStyle(target).lineHeight);
|
||||
textareaLineHeightRef.current = Number.isFinite(lineHeight) ? lineHeight : 24;
|
||||
}
|
||||
|
||||
const expanded = nextHeight > (textareaLineHeightRef.current || 24) * 2;
|
||||
setIsTextareaExpanded((previous) => previous === expanded ? previous : expanded);
|
||||
lastAutosizedInputRef.current = target.value;
|
||||
}, []);
|
||||
|
||||
const handleImageFiles = useCallback((files: File[]) => {
|
||||
const validFiles = files.filter((file) => {
|
||||
try {
|
||||
@@ -806,13 +824,13 @@ export function useChatComposerState({
|
||||
if (!textareaRef.current) {
|
||||
return;
|
||||
}
|
||||
// Re-run when input changes so restored drafts get the same autosize behavior as typed text.
|
||||
textareaRef.current.style.height = 'auto';
|
||||
textareaRef.current.style.height = `${Math.max(22, textareaRef.current.scrollHeight)}px`;
|
||||
const lineHeight = parseInt(window.getComputedStyle(textareaRef.current).lineHeight);
|
||||
const expanded = textareaRef.current.scrollHeight > lineHeight * 2;
|
||||
setIsTextareaExpanded(expanded);
|
||||
}, [input]);
|
||||
if (lastAutosizedInputRef.current === input) {
|
||||
return;
|
||||
}
|
||||
// Re-run for restored drafts and programmatic input changes. User typing is
|
||||
// already resized in onInput, so this avoids doing the same forced layout twice.
|
||||
resizeTextarea(textareaRef.current);
|
||||
}, [input, resizeTextarea]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!textareaRef.current || input.trim()) {
|
||||
@@ -894,15 +912,11 @@ export function useChatComposerState({
|
||||
const handleTextareaInput = useCallback(
|
||||
(event: FormEvent<HTMLTextAreaElement>) => {
|
||||
const target = event.currentTarget;
|
||||
target.style.height = 'auto';
|
||||
target.style.height = `${Math.max(22, target.scrollHeight)}px`;
|
||||
resizeTextarea(target);
|
||||
setCursorPosition(target.selectionStart);
|
||||
syncInputOverlayScroll(target);
|
||||
|
||||
const lineHeight = parseInt(window.getComputedStyle(target).lineHeight);
|
||||
setIsTextareaExpanded(target.scrollHeight > lineHeight * 2);
|
||||
},
|
||||
[setCursorPosition, syncInputOverlayScroll],
|
||||
[resizeTextarea, setCursorPosition, syncInputOverlayScroll],
|
||||
);
|
||||
|
||||
const handleClearInput = useCallback(() => {
|
||||
|
||||
@@ -309,7 +309,7 @@ function ChatInterface({
|
||||
|
||||
return (
|
||||
<PermissionContext.Provider value={permissionContextValue}>
|
||||
<div className="flex h-full flex-col">
|
||||
<div className="flex h-full min-h-0 flex-col">
|
||||
<ChatMessagesPane
|
||||
scrollContainerRef={scrollContainerRef}
|
||||
onWheel={handleScroll}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useMemo } from 'react';
|
||||
import type {
|
||||
ChangeEvent,
|
||||
ClipboardEvent,
|
||||
@@ -154,12 +155,17 @@ export default function ChatComposer({
|
||||
sendByCtrlEnter,
|
||||
}: ChatComposerProps) {
|
||||
const { t } = useTranslation('chat');
|
||||
const textareaRect = textareaRef.current?.getBoundingClientRect();
|
||||
const commandMenuPosition = {
|
||||
top: textareaRect ? Math.max(16, textareaRect.top - 316) : 0,
|
||||
left: textareaRect ? textareaRect.left : 16,
|
||||
bottom: textareaRect ? window.innerHeight - textareaRect.top + 8 : 90,
|
||||
};
|
||||
const commandMenuPosition = useMemo(() => {
|
||||
if (!isCommandMenuOpen) {
|
||||
return { top: 0, left: 16, bottom: 90 };
|
||||
}
|
||||
const textareaRect = textareaRef.current?.getBoundingClientRect();
|
||||
return {
|
||||
top: textareaRect ? Math.max(16, textareaRect.top - 316) : 0,
|
||||
left: textareaRect ? textareaRect.left : 16,
|
||||
bottom: textareaRect ? window.innerHeight - textareaRect.top + 8 : 90,
|
||||
};
|
||||
}, [input, isCommandMenuOpen, textareaRef]);
|
||||
|
||||
// Detect if the AskUserQuestion interactive panel is active
|
||||
const hasQuestionPanel = pendingPermissionRequests.some(
|
||||
@@ -170,7 +176,7 @@ export default function ChatComposer({
|
||||
const hasPendingPermissions = pendingPermissionRequests.length > 0;
|
||||
|
||||
return (
|
||||
<div className="flex-shrink-0 p-2 pb-2 sm:p-4 sm:pb-4 md:p-4 md:pb-6">
|
||||
<div className="chat-composer-shell flex-shrink-0 p-2 pb-2 sm:p-4 sm:pb-4 md:p-4 md:pb-6">
|
||||
{!hasPendingPermissions && (
|
||||
<ActivityIndicator activity={activity} onAbort={onAbortSession} />
|
||||
)}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useCallback, useMemo, useRef } from 'react';
|
||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
||||
import type { Dispatch, RefObject, SetStateAction } from 'react';
|
||||
|
||||
import type { ChatMessage } from '../../types/types';
|
||||
@@ -67,7 +67,7 @@ interface ChatMessagesPaneProps {
|
||||
selectedProject: Project;
|
||||
}
|
||||
|
||||
export default function ChatMessagesPane({
|
||||
function ChatMessagesPane({
|
||||
scrollContainerRef,
|
||||
onWheel,
|
||||
onTouchMove,
|
||||
@@ -151,7 +151,7 @@ export default function ChatMessagesPane({
|
||||
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"
|
||||
className="chat-messages-pane relative min-h-0 flex-1 space-y-3 overflow-y-auto overflow-x-hidden px-0 py-3 sm:space-y-4 sm:p-4"
|
||||
>
|
||||
{(isLoadingSessionMessages || isProcessing) && chatMessages.length === 0 ? (
|
||||
<div className="mt-8 text-center text-gray-500 dark:text-gray-400">
|
||||
@@ -308,3 +308,5 @@ export default function ChatMessagesPane({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default memo(ChatMessagesPane);
|
||||
|
||||
Reference in New Issue
Block a user