refactor(useChatComposerState): improve input handling and command execution flow

This commit is contained in:
Haileyesus
2026-02-12 21:25:46 +03:00
parent 7cabb3da9b
commit c79c022ff8

View File

@@ -129,6 +129,7 @@ export function useChatComposerState({
const handleSubmitRef = useRef< const handleSubmitRef = useRef<
((event: FormEvent<HTMLFormElement> | MouseEvent | TouchEvent | KeyboardEvent<HTMLTextAreaElement>) => Promise<void>) | null ((event: FormEvent<HTMLFormElement> | MouseEvent | TouchEvent | KeyboardEvent<HTMLTextAreaElement>) => Promise<void>) | null
>(null); >(null);
const inputValueRef = useRef(input);
const handleBuiltInCommand = useCallback( const handleBuiltInCommand = useCallback(
(result: CommandExecutionResult) => { (result: CommandExecutionResult) => {
@@ -258,13 +259,16 @@ export function useChatComposerState({
} }
} }
setInput(content || ''); const commandContent = content || '';
setInput(commandContent);
inputValueRef.current = commandContent;
// Defer submit to next tick so the command text is reflected in UI before dispatching.
setTimeout(() => { setTimeout(() => {
if (handleSubmitRef.current) { if (handleSubmitRef.current) {
handleSubmitRef.current(createFakeSubmitEvent()); handleSubmitRef.current(createFakeSubmitEvent());
} }
}, 50); }, 0);
}, [setChatMessages]); }, [setChatMessages]);
const executeCommand = useCallback( const executeCommand = useCallback(
@@ -279,7 +283,7 @@ export function useChatComposerState({
commandMatch && commandMatch[1] ? commandMatch[1].trim().split(/\s+/) : []; commandMatch && commandMatch[1] ? commandMatch[1].trim().split(/\s+/) : [];
const context = { const context = {
projectPath: selectedProject.path, projectPath: selectedProject.fullPath || selectedProject.path,
projectName: selectedProject.name, projectName: selectedProject.name,
sessionId: currentSessionId, sessionId: currentSessionId,
provider, provider,
@@ -314,11 +318,11 @@ export function useChatComposerState({
const result = (await response.json()) as CommandExecutionResult; const result = (await response.json()) as CommandExecutionResult;
if (result.type === 'builtin') { if (result.type === 'builtin') {
handleBuiltInCommand(result); handleBuiltInCommand(result);
setInput('');
inputValueRef.current = '';
} else if (result.type === 'custom') { } else if (result.type === 'custom') {
await handleCustomCommand(result); await handleCustomCommand(result);
} }
setInput('');
} catch (error) { } catch (error) {
const message = error instanceof Error ? error.message : 'Unknown error'; const message = error instanceof Error ? error.message : 'Unknown error';
console.error('Error executing command:', error); console.error('Error executing command:', error);
@@ -465,14 +469,15 @@ export function useChatComposerState({
event: FormEvent<HTMLFormElement> | MouseEvent | TouchEvent | KeyboardEvent<HTMLTextAreaElement>, event: FormEvent<HTMLFormElement> | MouseEvent | TouchEvent | KeyboardEvent<HTMLTextAreaElement>,
) => { ) => {
event.preventDefault(); event.preventDefault();
if (!input.trim() || isLoading || !selectedProject) { const currentInput = inputValueRef.current;
if (!currentInput.trim() || isLoading || !selectedProject) {
return; return;
} }
let messageContent = input; let messageContent = currentInput;
const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode); const selectedThinkingMode = thinkingModes.find((mode: { id: string; prefix?: string }) => mode.id === thinkingMode);
if (selectedThinkingMode && selectedThinkingMode.prefix) { if (selectedThinkingMode && selectedThinkingMode.prefix) {
messageContent = `${selectedThinkingMode.prefix}: ${input}`; messageContent = `${selectedThinkingMode.prefix}: ${currentInput}`;
} }
let uploadedImages: unknown[] = []; let uploadedImages: unknown[] = [];
@@ -512,7 +517,7 @@ export function useChatComposerState({
const userMessage: ChatMessage = { const userMessage: ChatMessage = {
type: 'user', type: 'user',
content: input, content: currentInput,
images: uploadedImages as any, images: uploadedImages as any,
timestamp: new Date(), timestamp: new Date(),
}; };
@@ -566,6 +571,7 @@ export function useChatComposerState({
}; };
const toolsSettings = getToolsSettings(); const toolsSettings = getToolsSettings();
const resolvedProjectPath = selectedProject.fullPath || selectedProject.path || '';
if (provider === 'cursor') { if (provider === 'cursor') {
sendMessage({ sendMessage({
@@ -573,8 +579,8 @@ export function useChatComposerState({
command: messageContent, command: messageContent,
sessionId: effectiveSessionId, sessionId: effectiveSessionId,
options: { options: {
cwd: selectedProject.fullPath || selectedProject.path, cwd: resolvedProjectPath,
projectPath: selectedProject.fullPath || selectedProject.path, projectPath: resolvedProjectPath,
sessionId: effectiveSessionId, sessionId: effectiveSessionId,
resume: Boolean(effectiveSessionId), resume: Boolean(effectiveSessionId),
model: cursorModel, model: cursorModel,
@@ -588,8 +594,8 @@ export function useChatComposerState({
command: messageContent, command: messageContent,
sessionId: effectiveSessionId, sessionId: effectiveSessionId,
options: { options: {
cwd: selectedProject.fullPath || selectedProject.path, cwd: resolvedProjectPath,
projectPath: selectedProject.fullPath || selectedProject.path, projectPath: resolvedProjectPath,
sessionId: effectiveSessionId, sessionId: effectiveSessionId,
resume: Boolean(effectiveSessionId), resume: Boolean(effectiveSessionId),
model: codexModel, model: codexModel,
@@ -601,10 +607,10 @@ export function useChatComposerState({
type: 'claude-command', type: 'claude-command',
command: messageContent, command: messageContent,
options: { options: {
projectPath: selectedProject.path, projectPath: resolvedProjectPath,
cwd: selectedProject.fullPath, cwd: resolvedProjectPath,
sessionId: currentSessionId, sessionId: effectiveSessionId,
resume: Boolean(currentSessionId), resume: Boolean(effectiveSessionId),
toolsSettings, toolsSettings,
permissionMode, permissionMode,
model: claudeModel, model: claudeModel,
@@ -614,6 +620,7 @@ export function useChatComposerState({
} }
setInput(''); setInput('');
inputValueRef.current = '';
resetCommandMenuState(); resetCommandMenuState();
setAttachedImages([]); setAttachedImages([]);
setUploadingImages(new Map()); setUploadingImages(new Map());
@@ -633,7 +640,6 @@ export function useChatComposerState({
codexModel, codexModel,
currentSessionId, currentSessionId,
cursorModel, cursorModel,
input,
isLoading, isLoading,
onSessionActive, onSessionActive,
pendingViewSessionRef, pendingViewSessionRef,
@@ -657,12 +663,20 @@ export function useChatComposerState({
handleSubmitRef.current = handleSubmit; handleSubmitRef.current = handleSubmit;
}, [handleSubmit]); }, [handleSubmit]);
useEffect(() => {
inputValueRef.current = input;
}, [input]);
useEffect(() => { useEffect(() => {
if (!selectedProject) { if (!selectedProject) {
return; return;
} }
const savedInput = safeLocalStorage.getItem(`draft_input_${selectedProject.name}`) || ''; const savedInput = safeLocalStorage.getItem(`draft_input_${selectedProject.name}`) || '';
setInput((previous) => (previous === savedInput ? previous : savedInput)); setInput((previous) => {
const next = previous === savedInput ? previous : savedInput;
inputValueRef.current = next;
return next;
});
}, [selectedProject?.name]); }, [selectedProject?.name]);
useEffect(() => { useEffect(() => {
@@ -680,12 +694,13 @@ export function useChatComposerState({
if (!textareaRef.current) { if (!textareaRef.current) {
return; 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 = 'auto';
textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`; textareaRef.current.style.height = `${textareaRef.current.scrollHeight}px`;
const lineHeight = parseInt(window.getComputedStyle(textareaRef.current).lineHeight); const lineHeight = parseInt(window.getComputedStyle(textareaRef.current).lineHeight);
const expanded = textareaRef.current.scrollHeight > lineHeight * 2; const expanded = textareaRef.current.scrollHeight > lineHeight * 2;
setIsTextareaExpanded(expanded); setIsTextareaExpanded(expanded);
}, []); }, [input]);
useEffect(() => { useEffect(() => {
if (!textareaRef.current || input.trim()) { if (!textareaRef.current || input.trim()) {
@@ -701,6 +716,7 @@ export function useChatComposerState({
const cursorPos = event.target.selectionStart; const cursorPos = event.target.selectionStart;
setInput(newValue); setInput(newValue);
inputValueRef.current = newValue;
setCursorPosition(cursorPos); setCursorPosition(cursorPos);
if (!newValue.trim()) { if (!newValue.trim()) {
@@ -779,6 +795,7 @@ export function useChatComposerState({
const handleClearInput = useCallback(() => { const handleClearInput = useCallback(() => {
setInput(''); setInput('');
inputValueRef.current = '';
resetCommandMenuState(); resetCommandMenuState();
if (textareaRef.current) { if (textareaRef.current) {
textareaRef.current.style.height = 'auto'; textareaRef.current.style.height = 'auto';
@@ -827,6 +844,7 @@ export function useChatComposerState({
setInput((previousInput) => { setInput((previousInput) => {
const newInput = previousInput.trim() ? `${previousInput} ${text}` : text; const newInput = previousInput.trim() ? `${previousInput} ${text}` : text;
inputValueRef.current = newInput;
setTimeout(() => { setTimeout(() => {
if (!textareaRef.current) { if (!textareaRef.current) {