diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 877053e..85ab729 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -59,7 +59,6 @@ function RootLayoutContent() { const [selectedModel, setSelectedModel] = useState("gemini-2.5-flash"); const [cliIOLogs, setCliIOLogs] = useState([]); - const messagesContainerRef = useRef(null); const [sidebarOpen, setSidebarOpen] = useState(true); const [directoryPanelOpen, setDirectoryPanelOpen] = useState(false); const [searchOpen, setSearchOpen] = useState(false); @@ -158,10 +157,19 @@ function RootLayoutContent() { updateConversation, }); + // Get yolo mode status from backend config + const isYoloEnabled = + backendState.selectedBackend === "gemini" + ? backendState.configs.gemini.yolo + : backendState.selectedBackend === "qwen" + ? backendState.configs.qwen.yolo + : false; + const { setupEventListenerForConversation } = useConversationEvents( setCliIOLogs, setConfirmationRequests, - updateConversation + updateConversation, + isYoloEnabled ); const { input, handleInputChange, handleSendMessage } = useMessageHandler({ @@ -450,7 +458,6 @@ function RootLayoutContent() { currentConversation, input, isCliInstalled, - messagesContainerRef, cliIOLogs, handleInputChange, handleSendMessage, @@ -468,7 +475,6 @@ function RootLayoutContent() { currentConversation, input, isCliInstalled, - messagesContainerRef, cliIOLogs, handleInputChange, handleSendMessage, diff --git a/frontend/src/contexts/ConversationContext.tsx b/frontend/src/contexts/ConversationContext.tsx index e9639b9..64f07dc 100644 --- a/frontend/src/contexts/ConversationContext.tsx +++ b/frontend/src/contexts/ConversationContext.tsx @@ -4,14 +4,16 @@ import { ToolCallConfirmationRequest } from "../utils/toolCallParser"; import { ConversationHistoryEntry } from "../lib/webApi"; import { SessionProgressPayload } from "../types/session"; -// Context for sharing conversation state with child routes +/** + * Context type for sharing conversation state with child routes. + * Provides conversation management, message handling, and tool call confirmation. + */ export interface ConversationContextType { conversations: Conversation[]; activeConversation: string | null; currentConversation: Conversation | undefined; input: string; isCliInstalled: boolean | null; - messagesContainerRef: React.RefObject; cliIOLogs: CliIO[]; handleInputChange: ( _event: React.ChangeEvent | null, @@ -43,6 +45,11 @@ export const ConversationContext = createContext< ConversationContextType | undefined >(undefined); +/** + * Custom hook to access the conversation context. + * @returns The conversation context value. + * @throws Error if used outside of a ConversationProvider. + */ export const useConversation = () => { const context = useContext(ConversationContext); if (context === undefined) { diff --git a/frontend/src/hooks/useConversationEvents.ts b/frontend/src/hooks/useConversationEvents.ts index 786af5f..0de5e11 100644 --- a/frontend/src/hooks/useConversationEvents.ts +++ b/frontend/src/hooks/useConversationEvents.ts @@ -2,6 +2,7 @@ import React, { useCallback, useRef } from "react"; import type { TextMessagePart } from "../types"; import { listen } from "@/lib/listen"; import { getWebSocketManager } from "../lib/webApi"; +import { api } from "../lib/api"; import { Conversation, Message, CliIO } from "../types"; import { ToolCallConfirmationRequest } from "../utils/toolCallParser"; import { type ToolCall } from "../utils/toolCallParser"; @@ -261,6 +262,16 @@ function getOptionKind( } } +/** + * Custom hook to set up event listeners for conversation events. + * Handles CLI I/O logging, tool call confirmations, and AI turn events. + * + * @param setCliIOLogs - State setter for CLI input/output logs + * @param setConfirmationRequests - State setter for tool call confirmation requests + * @param updateConversation - Function to update conversation state + * @param isYoloEnabled - Optional flag to enable yolo mode (auto-approve tool calls) + * @returns Object with setupEventListenerForConversation function + */ export const useConversationEvents = ( setCliIOLogs: React.Dispatch>, setConfirmationRequests: React.Dispatch< @@ -269,13 +280,24 @@ export const useConversationEvents = ( updateConversation: ( conversationId: string, updateFn: (conv: Conversation, lastMsg: Message) => void - ) => void + ) => void, + isYoloEnabled?: boolean ) => { // Buffer agent text chunks seen on CLI output in case the UI misses // ai-output streaming events (web WS race). Cleared on turn finish. const pendingAssistantTextRef = useRef>(new Map()); const sawAiOutputRef = useRef>(new Map()); - + // Use ref to avoid re-creating listeners when yolo mode toggles + const isYoloEnabledRef = useRef(isYoloEnabled ?? false); + isYoloEnabledRef.current = isYoloEnabled ?? false; + + /** + * Sets up event listeners for a specific conversation. + * Listens for CLI I/O events, tool call confirmations, and AI turn events. + * + * @param conversationId - The ID of the conversation to listen to + * @returns Cleanup function to remove all event listeners + */ const setupEventListenerForConversation = useCallback( async (conversationId: string): Promise<() => void> => { // In web mode, ensure WebSocket connection is ready before registering listeners @@ -898,19 +920,66 @@ export const useConversationEvents = ( : [], }; - setConfirmationRequests((prev) => { - const newMap = new Map(prev); - newMap.set(toolCallId, legacyConfirmationRequest); - console.log( - `✅ Stored confirmation request in Map for toolCallId:`, - toolCallId - ); + // If yolo mode is enabled, auto-approve the tool call + if (isYoloEnabledRef.current) { console.log( - `✅ Total confirmation requests in Map:`, - newMap.size + `🤖 [YOLO] Auto-approving tool call: ${toolCallId}`, + legacyConfirmationRequest ); - return newMap; - }); + // Find the best auto-approve option, preferring allow_always + // over allow_once so YOLO mode doesn't re-prompt for the same tool. + const autoApproveOption = + legacyConfirmationRequest.options?.find( + (opt) => opt.kind === "allow_always" + ) ?? + legacyConfirmationRequest.options?.find( + (opt) => opt.kind === "allow_once" + ); + const outcome = autoApproveOption?.optionId || "proceed_always"; + if (!request.sessionId) { + console.error( + `🤖 [YOLO] No sessionId found for tool call: ${toolCallId}` + ); + } else { + // Send approval response to backend + api + .send_tool_call_confirmation_response({ + sessionId: request.sessionId, + requestId: legacyConfirmationRequest.requestId, + toolCallId: toolCallId, + outcome: outcome, + }) + .catch((err) => { + console.error("Failed to send auto-approve response:", err); + // Fall back to showing the confirmation dialog so the user + // can manually approve and the call doesn't get stuck. + setConfirmationRequests((prev) => { + const newMap = new Map(prev); + newMap.set(toolCallId, legacyConfirmationRequest); + console.log( + `⚠️ [YOLO] Auto-approve failed, falling back to manual confirmation for toolCallId:`, + toolCallId + ); + return newMap; + }); + }); + } + } else { + // Only store confirmation request if yolo mode is disabled + setConfirmationRequests((prev) => { + const newMap = new Map(prev); + newMap.set(toolCallId, legacyConfirmationRequest); + console.log( + `✅ Stored confirmation request in Map for toolCallId:`, + toolCallId + ); + console.log( + `✅ Total confirmation requests in Map:`, + newMap.size + ); + return newMap; + }); + } } ); unlistenFunctions.push(unlistenAcpPermissionRequest); diff --git a/frontend/src/pages/HomeDashboard.tsx b/frontend/src/pages/HomeDashboard.tsx index 94da9f0..a92c764 100644 --- a/frontend/src/pages/HomeDashboard.tsx +++ b/frontend/src/pages/HomeDashboard.tsx @@ -33,19 +33,68 @@ import { GeminiMessagePart } from "../types"; export const HomeDashboard: React.FC = () => { const { t } = useTranslation(); const navigate = useNavigate(); - const { - currentConversation, - messagesContainerRef, - handleConfirmToolCall, - confirmationRequests, - } = useConversation(); - - // Debug logging for currentConversation - console.log("🏠 HomeDashboard - currentConversation:", currentConversation); + const { currentConversation, handleConfirmToolCall, confirmationRequests } = + useConversation(); const { selectedBackend } = useBackend(); const backendText = getBackendText(selectedBackend); + // Use a local ref for auto-scrolling + const localContainerRef = React.useRef(null); + + // Track if user is near bottom (for auto-scroll behavior) + const isNearBottomRef = React.useRef(true); + // Track previous scroll height to detect content growth + const prevScrollHeightRef = React.useRef(0); + + // Listen for scroll events to track user position + React.useEffect(() => { + const container = localContainerRef.current; + if (!container) return; + + const handleScroll = () => { + const threshold = 100; // pixels from bottom + const position = container.scrollTop + container.clientHeight; + const height = container.scrollHeight; + isNearBottomRef.current = height - position < threshold; + }; + + container.addEventListener("scroll", handleScroll, { passive: true }); + return () => container.removeEventListener("scroll", handleScroll); + }, []); + + // Auto-scroll to bottom when content grows (only if user is near bottom) + React.useEffect(() => { + const container = localContainerRef.current; + if (!container || !currentConversation?.messages.length) return; + + // Use ResizeObserver to detect when content height changes + const observer = new ResizeObserver(() => { + const newHeight = container.scrollHeight; + const prevHeight = prevScrollHeightRef.current; + + // Only scroll if content grew and user is near bottom + if (newHeight > prevHeight && isNearBottomRef.current) { + // Use requestAnimationFrame for smooth scrolling + requestAnimationFrame(() => { + container.scrollTop = container.scrollHeight; + }); + } + + prevScrollHeightRef.current = newHeight; + }); + + // Observe the content container (the div with space-y-8) + const contentContainer = container.firstElementChild; + if (contentContainer) { + observer.observe(contentContainer); + // Initial height capture + prevScrollHeightRef.current = container.scrollHeight; + } + + return () => observer.disconnect(); + }, [currentConversation?.messages.length]); + return ( <> {currentConversation ? ( @@ -53,7 +102,7 @@ export const HomeDashboard: React.FC = () => { ) : (
} + ref={localContainerRef} className="flex-1 min-h-0 overflow-y-auto p-6 relative" >