Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 25 additions & 15 deletions src/components/AgentExecution.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useState, useEffect, useRef } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
ArrowLeft,
Play,
StopCircle,
import {
ArrowLeft,
Play,
StopCircle,
Terminal,
AlertCircle,
Loader2,
Expand Down Expand Up @@ -34,6 +34,11 @@ import { useVirtualizer } from "@tanstack/react-virtual";
import { HooksEditor } from "./HooksEditor";
import { useTrackEvent, useComponentMetrics, useFeatureAdoptionTracking } from "@/hooks";
import { useTabState } from "@/hooks/useTabState";
import {
isImeComposingKeydown,
createCompositionHandlers,
type IMECompositionRefs,
} from "@/utils/ime";

interface AgentExecutionProps {
/**
Expand Down Expand Up @@ -109,7 +114,8 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
const [isHooksDialogOpen, setIsHooksDialogOpen] = useState(false);

// IME composition state
const isIMEComposingRef = useRef(false);
const isComposingRef = useRef(false);
const justEndedRef = useRef(false);
const [activeHooksTab, setActiveHooksTab] = useState("project");

// Execution stats
Expand Down Expand Up @@ -431,15 +437,13 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
}
};

const handleCompositionStart = () => {
isIMEComposingRef.current = true;
};

const handleCompositionEnd = () => {
setTimeout(() => {
isIMEComposingRef.current = false;
}, 0);
};
// IME composition handlers using shared utility
const imeRefs: IMECompositionRefs = { isComposingRef, justEndedRef };
const {
onCompositionStart: handleCompositionStart,
onCompositionEnd: handleCompositionEnd,
onBlur: handleBlur,
} = createCompositionHandlers(imeRefs);

const handleBackWithConfirmation = () => {
if (isRunning) {
Expand Down Expand Up @@ -680,15 +684,21 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
disabled={isRunning}
className="flex-1 h-9"
onKeyDown={(e) => {
if (justEndedRef.current && e.key !== "Enter") {
justEndedRef.current = false;
}
if (e.key === "Enter" && !isRunning && projectPath && task.trim()) {
if (e.nativeEvent.isComposing || isIMEComposingRef.current) {
const composing = isImeComposingKeydown(e, isComposingRef);
if (composing || justEndedRef.current) {
justEndedRef.current = false;
return;
}
handleExecute();
}
}}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onBlur={handleBlur}
/>
<motion.div
whileTap={{ scale: 0.97 }}
Expand Down
36 changes: 24 additions & 12 deletions src/components/ClaudeCodeSession.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { useState, useEffect, useRef, useMemo } from "react";
import { motion, AnimatePresence } from "framer-motion";
import {
import {
Copy,
ChevronDown,
GitBranch,
Expand All @@ -15,6 +15,11 @@ import { Label } from "@/components/ui/label";
import { Popover } from "@/components/ui/popover";
import { api, type Session } from "@/lib/api";
import { cn } from "@/lib/utils";
import {
isImeComposingKeydown,
createCompositionHandlers,
type IMECompositionRefs,
} from "@/utils/ime";

// Conditional imports for Tauri APIs
let tauriListen: any;
Expand Down Expand Up @@ -145,7 +150,9 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
const isMountedRef = useRef(true);
const isListeningRef = useRef(false);
const sessionStartTime = useRef<number>(Date.now());
const isIMEComposingRef = useRef(false);
// Tracks IME composition state
const isComposingRef = useRef(false);
const justEndedRef = useRef(false);

// Session metrics state for enhanced analytics
const sessionMetrics = useRef({
Expand Down Expand Up @@ -1096,15 +1103,13 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
setShowForkDialog(true);
};

const handleCompositionStart = () => {
isIMEComposingRef.current = true;
};

const handleCompositionEnd = () => {
setTimeout(() => {
isIMEComposingRef.current = false;
}, 0);
};
// IME composition handlers using shared utility
const imeRefs: IMECompositionRefs = { isComposingRef, justEndedRef };
const {
onCompositionStart: handleCompositionStart,
onCompositionEnd: handleCompositionEnd,
onBlur: handleBlur,
} = createCompositionHandlers(imeRefs);

const handleConfirmFork = async () => {
if (!forkCheckpointId || !forkSessionName.trim() || !effectiveSession) return;
Expand Down Expand Up @@ -1695,15 +1700,22 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
value={forkSessionName}
onChange={(e) => setForkSessionName(e.target.value)}
onKeyDown={(e) => {
// Reset justEnded flag on non-Enter keys
if (justEndedRef.current && e.key !== "Enter") {
justEndedRef.current = false;
}
if (e.key === "Enter" && !isLoading) {
if (e.nativeEvent.isComposing || isIMEComposingRef.current) {
const composing = isImeComposingKeydown(e, isComposingRef);
if (composing || justEndedRef.current) {
justEndedRef.current = false;
return;
}
handleConfirmFork();
}
}}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onBlur={handleBlur}
/>
</div>
</div>
Expand Down
76 changes: 31 additions & 45 deletions src/components/FloatingPromptInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Lightbulb,
Cpu,
Rocket,

} from "lucide-react";
import { cn } from "@/lib/utils";
import { Button } from "@/components/ui/button";
Expand All @@ -23,6 +23,11 @@ import { FilePicker } from "./FilePicker";
import { SlashCommandPicker } from "./SlashCommandPicker";
import { ImagePreview } from "./ImagePreview";
import { type FileEntry, type SlashCommand } from "@/lib/api";
import {
isImeComposingKeydown,
createCompositionHandlers,
type IMECompositionRefs,
} from "@/utils/ime";

// Conditional import for Tauri webview window
let tauriGetCurrentWebviewWindow: any;
Expand Down Expand Up @@ -242,7 +247,10 @@ const FloatingPromptInputInner = (
const expandedTextareaRef = useRef<HTMLTextAreaElement>(null);
const unlistenDragDropRef = useRef<(() => void) | null>(null);
const [textareaHeight, setTextareaHeight] = useState<number>(48);
const isIMEComposingRef = useRef(false);
// Tracks IME composition state (between compositionstart and compositionend)
const isComposingRef = useRef(false);
// Flag to skip the first Enter after compositionend (one-shot)
const justEndedRef = useRef(false);

// Expose a method to add images programmatically
React.useImperativeHandle(
Expand Down Expand Up @@ -653,49 +661,17 @@ 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;
};
// IME composition handlers using shared utility
const imeRefs: IMECompositionRefs = { isComposingRef, justEndedRef };
const {
onCompositionStart: handleCompositionStart,
onCompositionEnd: handleCompositionEnd,
onBlur: handleBlur,
} = createCompositionHandlers(imeRefs);

const handleSend = () => {
if (isIMEInteraction()) {
return;
}
// handleSend is called from button click, no IME check needed
// keydown path checks in handleKeyDown

if (prompt.trim() && !disabled) {
let finalPrompt = prompt.trim();
Expand All @@ -714,6 +690,11 @@ const FloatingPromptInputInner = (
};

const handleKeyDown = (e: React.KeyboardEvent) => {
// Reset justEnded flag on non-Enter keys
if (justEndedRef.current && e.key !== "Enter") {
justEndedRef.current = false;
}

if (showFilePicker && e.key === 'Escape') {
e.preventDefault();
setShowFilePicker(false);
Expand All @@ -728,7 +709,7 @@ const FloatingPromptInputInner = (
return;
}

// Add keyboard shortcut for expanding
// Keyboard shortcut for expanding
if (e.key === 'e' && (e.ctrlKey || e.metaKey) && e.shiftKey) {
e.preventDefault();
setIsExpanded(true);
Expand All @@ -742,7 +723,10 @@ const FloatingPromptInputInner = (
!showFilePicker &&
!showSlashCommandPicker
) {
if (isIMEInteraction(e)) {
// Skip if IME composing or just ended composition
const composing = isImeComposingKeydown(e, isComposingRef);
if (composing || justEndedRef.current) {
justEndedRef.current = false;
return;
}
e.preventDefault();
Expand Down Expand Up @@ -901,6 +885,7 @@ const FloatingPromptInputInner = (
onChange={handleTextChange}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onBlur={handleBlur}
onPaste={handlePaste}
placeholder="Type your message..."
className="min-h-[200px] resize-none"
Expand Down Expand Up @@ -1226,6 +1211,7 @@ const FloatingPromptInputInner = (
onKeyDown={handleKeyDown}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onBlur={handleBlur}
onPaste={handlePaste}
placeholder={
dragActive
Expand Down
40 changes: 25 additions & 15 deletions src/components/TimelineNavigator.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import React, { useState, useEffect } from "react";
import { motion } from "framer-motion";
import {
GitBranch,
Save,
RotateCcw,
import {
GitBranch,
Save,
RotateCcw,
GitFork,
AlertCircle,
ChevronDown,
Expand All @@ -23,6 +23,11 @@ import { api, type Checkpoint, type TimelineNode, type SessionTimeline, type Che
import { cn } from "@/lib/utils";
import { formatDistanceToNow } from "date-fns";
import { useTrackEvent } from "@/hooks";
import {
isImeComposingKeydown,
createCompositionHandlers,
type IMECompositionRefs,
} from "@/utils/ime";

interface TimelineNavigatorProps {
sessionId: string;
Expand Down Expand Up @@ -72,7 +77,8 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
const trackEvent = useTrackEvent();

// IME composition state
const isIMEComposingRef = React.useRef(false);
const isComposingRef = React.useRef(false);
const justEndedRef = React.useRef(false);

// Load timeline on mount and whenever refreshVersion bumps
useEffect(() => {
Expand Down Expand Up @@ -196,15 +202,13 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
onFork(checkpoint.id);
};

const handleCompositionStart = () => {
isIMEComposingRef.current = true;
};

const handleCompositionEnd = () => {
setTimeout(() => {
isIMEComposingRef.current = false;
}, 0);
};
// IME composition handlers using shared utility
const imeRefs: IMECompositionRefs = { isComposingRef, justEndedRef };
const {
onCompositionStart: handleCompositionStart,
onCompositionEnd: handleCompositionEnd,
onBlur: handleBlur,
} = createCompositionHandlers(imeRefs);

const handleCompare = async (checkpoint: Checkpoint) => {
if (!selectedCheckpoint) {
Expand Down Expand Up @@ -495,15 +499,21 @@ export const TimelineNavigator: React.FC<TimelineNavigatorProps> = ({
value={checkpointDescription}
onChange={(e) => setCheckpointDescription(e.target.value)}
onKeyDown={(e) => {
if (justEndedRef.current && e.key !== "Enter") {
justEndedRef.current = false;
}
if (e.key === "Enter" && !isLoading) {
if (e.nativeEvent.isComposing || isIMEComposingRef.current) {
const composing = isImeComposingKeydown(e, isComposingRef);
if (composing || justEndedRef.current) {
justEndedRef.current = false;
return;
}
handleCreateCheckpoint();
}
}}
onCompositionStart={handleCompositionStart}
onCompositionEnd={handleCompositionEnd}
onBlur={handleBlur}
/>
</div>
</div>
Expand Down
Loading