Skip to content
Merged
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
65 changes: 21 additions & 44 deletions src/components/AgentExecution.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,9 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({

// Hooks configuration state
const [isHooksDialogOpen, setIsHooksDialogOpen] = useState(false);

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

// Execution stats
Expand Down Expand Up @@ -413,62 +416,31 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({

// 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
Expand Down Expand Up @@ -707,11 +679,16 @@ export const AgentExecution: React.FC<AgentExecutionProps> = ({
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}
/>
<motion.div
whileTap={{ scale: 0.97 }}
Expand Down
82 changes: 64 additions & 18 deletions src/components/ClaudeCodeSession.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({

// Add collapsed state for queued prompts
const [queuedPromptsCollapsed, setQueuedPromptsCollapsed] = useState(false);

const parentRef = useRef<HTMLDivElement>(null);
const unlistenRefs = useRef<UnlistenFn[]>([]);
const hasActiveSessionRef = useRef(false);
Expand All @@ -114,6 +114,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
const isMountedRef = useRef(true);
const isListeningRef = useRef(false);
const sessionStartTime = useRef<number>(Date.now());
const isIMEComposingRef = useRef(false);

// Session metrics state for enhanced analytics
const sessionMetrics = useRef({
Expand Down Expand Up @@ -274,7 +275,22 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
// 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]);

Expand Down Expand Up @@ -326,7 +342,17 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
// 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) {
Expand Down Expand Up @@ -1026,6 +1052,16 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
setShowForkDialog(true);
};

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

const handleCompositionEnd = () => {
setTimeout(() => {
isIMEComposingRef.current = false;
}, 0);
};

const handleConfirmFork = async () => {
if (!forkCheckpointId || !forkSessionName.trim() || !effectiveSession) return;

Expand Down Expand Up @@ -1141,7 +1177,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
const messagesList = (
<div
ref={parentRef}
className="flex-1 overflow-y-auto relative pb-40"
className="flex-1 overflow-y-auto relative pb-20"
style={{
contain: 'strict',
}}
Expand Down Expand Up @@ -1187,7 +1223,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
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"
>
<div className="rotating-symbol text-primary" />
</motion.div>
Expand All @@ -1199,7 +1235,7 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
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}
</motion.div>
Expand Down Expand Up @@ -1409,18 +1445,23 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
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"
>
<ChevronDown className="h-4 w-4" />
Expand Down Expand Up @@ -1609,11 +1650,16 @@ export const ClaudeCodeSession: React.FC<ClaudeCodeSessionProps> = ({
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}
/>
</div>
</div>
Expand Down
Loading