diff --git a/src/components/AgentExecution.tsx b/src/components/AgentExecution.tsx index 41fbb1fd7..90f089ccd 100644 --- a/src/components/AgentExecution.tsx +++ b/src/components/AgentExecution.tsx @@ -107,6 +107,9 @@ export const AgentExecution: React.FC = ({ // Hooks configuration state const [isHooksDialogOpen, setIsHooksDialogOpen] = useState(false); + + // IME composition state + const isIMEComposingRef = useRef(false); const [activeHooksTab, setActiveHooksTab] = useState("project"); // Execution stats @@ -413,62 +416,31 @@ export const AgentExecution: React.FC = ({ // Call the API to kill the agent session const success = await api.killAgentSession(runId); - + if (success) { console.log(`Successfully stopped agent session ${runId}`); } else { console.warn(`Failed to stop agent session ${runId} - it may have already finished`); } - + // Update UI state setIsRunning(false); setExecutionStartTime(null); - // Update tab status to idle when stopped - if (tabId) { - updateTabStatus(tabId, 'idle'); - } - - // Clean up listeners - unlistenRefs.current.forEach(unlisten => unlisten()); - unlistenRefs.current = []; - - // Add a message indicating execution was stopped - setMessages(prev => [...prev, { - type: "result", - subtype: "error", - is_error: true, - result: "Execution stopped by user", - duration_ms: elapsedTime * 1000, - usage: { - input_tokens: totalTokens, - output_tokens: 0 - } - }]); } catch (err) { console.error("Failed to stop agent:", err); - // Still update UI state even if the backend call failed - setIsRunning(false); - setExecutionStartTime(null); - // Update tab status to idle - if (tabId) { - updateTabStatus(tabId, 'idle'); - } - - // Show error message - setMessages(prev => [...prev, { - type: "result", - subtype: "error", - is_error: true, - result: `Failed to stop execution: ${err instanceof Error ? err.message : 'Unknown error'}`, - duration_ms: elapsedTime * 1000, - usage: { - input_tokens: totalTokens, - output_tokens: 0 - } - }]); } }; + const handleCompositionStart = () => { + isIMEComposingRef.current = true; + }; + + const handleCompositionEnd = () => { + setTimeout(() => { + isIMEComposingRef.current = false; + }, 0); + }; + const handleBackWithConfirmation = () => { if (isRunning) { // Show confirmation dialog before navigating away during execution @@ -707,11 +679,16 @@ export const AgentExecution: React.FC = ({ placeholder="What would you like the agent to do?" disabled={isRunning} className="flex-1 h-9" - onKeyPress={(e) => { + onKeyDown={(e) => { if (e.key === "Enter" && !isRunning && projectPath && task.trim()) { + if (e.nativeEvent.isComposing || isIMEComposingRef.current) { + return; + } handleExecute(); } }} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} /> = ({ // Add collapsed state for queued prompts const [queuedPromptsCollapsed, setQueuedPromptsCollapsed] = useState(false); - + const parentRef = useRef(null); const unlistenRefs = useRef([]); const hasActiveSessionRef = useRef(false); @@ -114,6 +114,7 @@ export const ClaudeCodeSession: React.FC = ({ const isMountedRef = useRef(true); const isListeningRef = useRef(false); const sessionStartTime = useRef(Date.now()); + const isIMEComposingRef = useRef(false); // Session metrics state for enhanced analytics const sessionMetrics = useRef({ @@ -274,7 +275,22 @@ export const ClaudeCodeSession: React.FC = ({ // Auto-scroll to bottom when new messages arrive useEffect(() => { if (displayableMessages.length > 0) { - rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: 'end', behavior: 'smooth' }); + // Use a more precise scrolling method to ensure content is fully visible + setTimeout(() => { + const scrollElement = parentRef.current; + if (scrollElement) { + // First, scroll using virtualizer to get close to the bottom + rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: 'end', behavior: 'auto' }); + + // Then use direct scroll to ensure we reach the absolute bottom + requestAnimationFrame(() => { + scrollElement.scrollTo({ + top: scrollElement.scrollHeight, + behavior: 'smooth' + }); + }); + } + }, 50); } }, [displayableMessages.length, rowVirtualizer]); @@ -326,7 +342,17 @@ export const ClaudeCodeSession: React.FC = ({ // Scroll to bottom after loading history setTimeout(() => { if (loadedMessages.length > 0) { - rowVirtualizer.scrollToIndex(loadedMessages.length - 1, { align: 'end', behavior: 'auto' }); + const scrollElement = parentRef.current; + if (scrollElement) { + // Use the same improved scrolling method + rowVirtualizer.scrollToIndex(loadedMessages.length - 1, { align: 'end', behavior: 'auto' }); + requestAnimationFrame(() => { + scrollElement.scrollTo({ + top: scrollElement.scrollHeight, + behavior: 'auto' + }); + }); + } } }, 100); } catch (err) { @@ -1026,6 +1052,16 @@ export const ClaudeCodeSession: React.FC = ({ setShowForkDialog(true); }; + const handleCompositionStart = () => { + isIMEComposingRef.current = true; + }; + + const handleCompositionEnd = () => { + setTimeout(() => { + isIMEComposingRef.current = false; + }, 0); + }; + const handleConfirmFork = async () => { if (!forkCheckpointId || !forkSessionName.trim() || !effectiveSession) return; @@ -1141,7 +1177,7 @@ export const ClaudeCodeSession: React.FC = ({ const messagesList = (
= ({ initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.15 }} - className="flex items-center justify-center py-4 mb-40" + className="flex items-center justify-center py-4 mb-20" >
@@ -1199,7 +1235,7 @@ export const ClaudeCodeSession: React.FC = ({ initial={{ opacity: 0, y: 8 }} animate={{ opacity: 1, y: 0 }} transition={{ duration: 0.15 }} - className="rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive mb-40 w-full max-w-6xl mx-auto" + className="rounded-lg border border-destructive/50 bg-destructive/10 p-4 text-sm text-destructive mb-20 w-full max-w-6xl mx-auto" > {error} @@ -1409,18 +1445,23 @@ export const ClaudeCodeSession: React.FC = ({ variant="ghost" size="sm" onClick={() => { - // Use virtualizer to scroll to the last item - if (displayableMessages.length > 0) { - // Scroll to bottom of the container - const scrollElement = parentRef.current; - if (scrollElement) { - scrollElement.scrollTo({ - top: scrollElement.scrollHeight, - behavior: 'smooth' - }); + // Use the improved scrolling method for manual scroll to bottom + if (displayableMessages.length > 0) { + const scrollElement = parentRef.current; + if (scrollElement) { + // First, scroll using virtualizer to get close to the bottom + rowVirtualizer.scrollToIndex(displayableMessages.length - 1, { align: 'end', behavior: 'auto' }); + + // Then use direct scroll to ensure we reach the absolute bottom + requestAnimationFrame(() => { + scrollElement.scrollTo({ + top: scrollElement.scrollHeight, + behavior: 'smooth' + }); + }); + } } - } - }} + }} className="px-3 py-2 hover:bg-accent rounded-none" > @@ -1609,11 +1650,16 @@ export const ClaudeCodeSession: React.FC = ({ placeholder="e.g., Alternative approach" value={forkSessionName} onChange={(e) => setForkSessionName(e.target.value)} - onKeyPress={(e) => { + onKeyDown={(e) => { if (e.key === "Enter" && !isLoading) { + if (e.nativeEvent.isComposing || isIMEComposingRef.current) { + return; + } handleConfirmFork(); } }} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} />
diff --git a/src/components/FloatingPromptInput.tsx b/src/components/FloatingPromptInput.tsx index c3f5ea28c..bc49cb8d1 100644 --- a/src/components/FloatingPromptInput.tsx +++ b/src/components/FloatingPromptInput.tsx @@ -230,6 +230,7 @@ const FloatingPromptInputInner = ( const expandedTextareaRef = useRef(null); const unlistenDragDropRef = useRef<(() => void) | null>(null); const [textareaHeight, setTextareaHeight] = useState(48); + const isIMEComposingRef = useRef(false); // Expose a method to add images programmatically React.useImperativeHandle( @@ -432,23 +433,6 @@ const FloatingPromptInputInner = ( } }, [isExpanded]); - const handleSend = () => { - if (prompt.trim() && !disabled) { - let finalPrompt = prompt.trim(); - - // Append thinking phrase if not auto mode - const thinkingMode = THINKING_MODES.find(m => m.id === selectedThinkingMode); - if (thinkingMode && thinkingMode.phrase) { - finalPrompt = `${finalPrompt}.\n\n${thinkingMode.phrase}.`; - } - - onSend(finalPrompt, selectedModel); - setPrompt(""); - setEmbeddedImages([]); - setTextareaHeight(48); // Reset height after sending - } - }; - const handleTextChange = (e: React.ChangeEvent) => { const newValue = e.target.value; const newCursorPosition = e.target.selectionStart || 0; @@ -657,6 +641,66 @@ const FloatingPromptInputInner = ( }, 0); }; + const handleCompositionStart = () => { + isIMEComposingRef.current = true; + }; + + const handleCompositionEnd = () => { + setTimeout(() => { + isIMEComposingRef.current = false; + }, 0); + }; + + const isIMEInteraction = (event?: React.KeyboardEvent) => { + if (isIMEComposingRef.current) { + return true; + } + + if (!event) { + return false; + } + + const nativeEvent = event.nativeEvent; + + if (nativeEvent.isComposing) { + return true; + } + + const key = nativeEvent.key; + if (key === 'Process' || key === 'Unidentified') { + return true; + } + + const keyboardEvent = nativeEvent as unknown as KeyboardEvent; + const keyCode = keyboardEvent.keyCode ?? (keyboardEvent as unknown as { which?: number }).which; + if (keyCode === 229) { + return true; + } + + return false; + }; + + const handleSend = () => { + if (isIMEInteraction()) { + return; + } + + if (prompt.trim() && !disabled) { + let finalPrompt = prompt.trim(); + + // Append thinking phrase if not auto mode + const thinkingMode = THINKING_MODES.find(m => m.id === selectedThinkingMode); + if (thinkingMode && thinkingMode.phrase) { + finalPrompt = `${finalPrompt}.\n\n${thinkingMode.phrase}.`; + } + + onSend(finalPrompt, selectedModel); + setPrompt(""); + setEmbeddedImages([]); + setTextareaHeight(48); // Reset height after sending + } + }; + const handleKeyDown = (e: React.KeyboardEvent) => { if (showFilePicker && e.key === 'Escape') { e.preventDefault(); @@ -679,7 +723,16 @@ const FloatingPromptInputInner = ( return; } - if (e.key === "Enter" && !e.shiftKey && !isExpanded && !showFilePicker && !showSlashCommandPicker) { + if ( + e.key === "Enter" && + !e.shiftKey && + !isExpanded && + !showFilePicker && + !showSlashCommandPicker + ) { + if (isIMEInteraction(e)) { + return; + } e.preventDefault(); handleSend(); } @@ -834,6 +887,8 @@ const FloatingPromptInputInner = ( ref={expandedTextareaRef} value={prompt} onChange={handleTextChange} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} onPaste={handlePaste} placeholder="Type your message..." className="min-h-[200px] resize-none" @@ -1157,15 +1212,21 @@ const FloatingPromptInputInner = ( value={prompt} onChange={handleTextChange} onKeyDown={handleKeyDown} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} onPaste={handlePaste} - placeholder={dragActive ? "Drop images here..." : "Message Claude (@ for files, / for commands)..."} + placeholder={ + dragActive + ? "Drop images here..." + : "Message Claude (@ for files, / for commands)..." + } disabled={disabled} className={cn( "resize-none pr-20 pl-3 py-2.5 transition-all duration-150", dragActive && "border-primary", textareaHeight >= 240 && "overflow-y-auto scrollbar-thin" )} - style={{ + style={{ height: `${textareaHeight}px`, overflowY: textareaHeight >= 240 ? 'auto' : 'hidden' }} diff --git a/src/components/TimelineNavigator.tsx b/src/components/TimelineNavigator.tsx index b4fea7578..2cc0ce154 100644 --- a/src/components/TimelineNavigator.tsx +++ b/src/components/TimelineNavigator.tsx @@ -67,10 +67,13 @@ export const TimelineNavigator: React.FC = ({ const [error, setError] = useState(null); const [diff, setDiff] = useState(null); const [compareCheckpoint, setCompareCheckpoint] = useState(null); - + // Analytics tracking const trackEvent = useTrackEvent(); + // IME composition state + const isIMEComposingRef = React.useRef(false); + // Load timeline on mount and whenever refreshVersion bumps useEffect(() => { loadTimeline(); @@ -193,6 +196,16 @@ export const TimelineNavigator: React.FC = ({ onFork(checkpoint.id); }; + const handleCompositionStart = () => { + isIMEComposingRef.current = true; + }; + + const handleCompositionEnd = () => { + setTimeout(() => { + isIMEComposingRef.current = false; + }, 0); + }; + const handleCompare = async (checkpoint: Checkpoint) => { if (!selectedCheckpoint) { setSelectedCheckpoint(checkpoint); @@ -481,11 +494,16 @@ export const TimelineNavigator: React.FC = ({ placeholder="e.g., Before major refactoring" value={checkpointDescription} onChange={(e) => setCheckpointDescription(e.target.value)} - onKeyPress={(e) => { + onKeyDown={(e) => { if (e.key === "Enter" && !isLoading) { + if (e.nativeEvent.isComposing || isIMEComposingRef.current) { + return; + } handleCreateCheckpoint(); } }} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} /> diff --git a/src/components/WebviewPreview.tsx b/src/components/WebviewPreview.tsx index f48fc8f29..f99807f4b 100644 --- a/src/components/WebviewPreview.tsx +++ b/src/components/WebviewPreview.tsx @@ -69,13 +69,14 @@ const WebviewPreviewComponent: React.FC = ({ // TODO: These will be implemented with actual webview navigation // const [canGoBack, setCanGoBack] = useState(false); // const [canGoForward, setCanGoForward] = useState(false); - + // TODO: These will be used for actual Tauri webview implementation // const webviewRef = useRef(null); const iframeRef = useRef(null); const containerRef = useRef(null); const contentRef = useRef(null); // const previewId = useRef(`preview-${Date.now()}`); + const isIMEComposingRef = useRef(false); // Handle ESC key to exit full screen useEffect(() => { @@ -142,8 +143,21 @@ const WebviewPreviewComponent: React.FC = ({ } }; + const handleCompositionStart = () => { + isIMEComposingRef.current = true; + }; + + const handleCompositionEnd = () => { + setTimeout(() => { + isIMEComposingRef.current = false; + }, 0); + }; + const handleKeyDown = (e: React.KeyboardEvent) => { if (e.key === 'Enter') { + if (e.nativeEvent.isComposing || isIMEComposingRef.current) { + return; + } handleNavigate(); } }; @@ -270,6 +284,8 @@ const WebviewPreviewComponent: React.FC = ({ value={inputUrl} onChange={(e) => setInputUrl(e.target.value)} onKeyDown={handleKeyDown} + onCompositionStart={handleCompositionStart} + onCompositionEnd={handleCompositionEnd} placeholder="Enter URL..." className="pr-10 h-8 text-sm font-mono" />