diff --git a/ui/desktop/src/App.test.tsx b/ui/desktop/src/App.test.tsx index 1955821280d4..92313dfcf8cc 100644 --- a/ui/desktop/src/App.test.tsx +++ b/ui/desktop/src/App.test.tsx @@ -131,18 +131,11 @@ vi.mock('./contexts/ChatContext', () => ({ hasActiveSession: false, setRecipe: vi.fn(), clearRecipe: vi.fn(), - draft: '', - setDraft: vi.fn(), - clearDraft: vi.fn(), contextKey: 'hub', }), DEFAULT_CHAT_TITLE: 'New Chat', // Keep this from HEAD })); -vi.mock('./contexts/DraftContext', () => ({ - DraftProvider: ({ children }: { children: React.ReactNode }) => <>{children}, -})); - vi.mock('./components/ui/ConfirmationModal', () => ({ ConfirmationModal: () => null, })); diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 5ae00641abc1..90199c4dd45b 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -27,7 +27,6 @@ import SchedulesView from './components/schedule/SchedulesView'; import ProviderSettings from './components/settings/providers/ProviderSettingsPage'; import { AppLayout } from './components/Layout/AppLayout'; import { ChatProvider } from './contexts/ChatContext'; -import { DraftProvider } from './contexts/DraftContext'; import 'react-toastify/dist/ReactToastify.css'; import { useConfig } from './components/ConfigContext'; @@ -638,13 +637,11 @@ export function AppInner() { export default function App() { return ( - - - - - - - - + + + + + + ); } diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 863c7f65b5b7..63da447e0ce2 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -20,7 +20,6 @@ import { WaveformVisualizer } from './WaveformVisualizer'; import { toastError } from '../toasts'; import MentionPopover, { FileItemWithMatch } from './MentionPopover'; import { useDictationSettings } from '../hooks/useDictationSettings'; -import { useChatContext } from '../contexts/ChatContext'; import { COST_TRACKING_ENABLED, VOICE_DICTATION_ELEVENLABS_ENABLED } from '../updates'; import { CostTracker } from './bottom_menu/CostTracker'; import { DroppedFile, useFileDrop } from '../hooks/useFileDrop'; @@ -142,20 +141,8 @@ export default function ChatInput({ const { getCurrentModelAndProvider, currentModel, currentProvider } = useModelAndProvider(); const [tokenLimit, setTokenLimit] = useState(TOKEN_LIMIT_DEFAULT); const [isTokenLimitLoaded, setIsTokenLimitLoaded] = useState(false); - - // Draft functionality - get chat context and global draft context - // We need to handle the case where ChatInput is used without ChatProvider (e.g., in Hub) - const chatContext = useChatContext(); // This should always be available now - const agentIsReady = chatContext === null || chatContext.agentWaitingMessage === null; - const draftLoadedRef = useRef(false); - const [diagnosticsOpen, setDiagnosticsOpen] = useState(false); - // Debug logging for draft context - useEffect(() => { - // Debug logging removed - draft functionality is working correctly - }, [chatContext?.contextKey, chatContext?.draft, chatContext]); - // Save queue state (paused/interrupted) to storage useEffect(() => { try { @@ -281,9 +268,6 @@ export default function ChatInput({ setValue(initialValue); setDisplayValue(initialValue); - // Reset draft loaded flag when initialValue changes - draftLoadedRef.current = false; - // Use a functional update to get the current pastedImages // and perform cleanup. This avoids needing pastedImages in the deps. setPastedImages((currentPastedImages) => { @@ -313,38 +297,6 @@ export default function ChatInput({ } }, [recipeAccepted, initialPrompt, messages.length]); - // Draft functionality - load draft if no initial value or recipe - useEffect(() => { - // Reset draft loaded flag when context changes - draftLoadedRef.current = false; - }, [chatContext?.contextKey]); - - useEffect(() => { - // Only load draft once and if conditions are met - if (!initialValue && !recipe && !draftLoadedRef.current && chatContext) { - const draftText = chatContext.draft || ''; - - if (draftText) { - setDisplayValue(draftText); - setValue(draftText); - } - - // Always mark as loaded after checking, regardless of whether we found a draft - draftLoadedRef.current = true; - } - }, [chatContext, initialValue, recipe]); - - // Save draft when user types (debounced) - const debouncedSaveDraft = useMemo( - () => - debounce((value: string) => { - if (chatContext && chatContext.setDraft) { - chatContext.setDraft(value); - } - }, 500), // Save draft after 500ms of no typing - [chatContext] - ); - // State to track if the IME is composing (i.e., in the middle of Japanese IME input) const [isComposing, setIsComposing] = useState(false); const [historyIndex, setHistoryIndex] = useState(-1); @@ -608,13 +560,9 @@ export default function ChatInput({ const val = evt.target.value; const cursorPosition = evt.target.selectionStart; - setDisplayValue(val); // Update display immediately - updateValue(val); // Update actual value immediately for better responsiveness - debouncedSaveDraft(val); // Save draft with debounce - // Mark that the user has typed something + setDisplayValue(val); + updateValue(val); setHasUserTyped(true); - - // Check for @ mention checkForMention(val, cursorPosition, evt.target); }; @@ -769,9 +717,8 @@ export default function ChatInput({ useEffect(() => { return () => { debouncedAutosize.cancel?.(); - debouncedSaveDraft.cancel?.(); }; - }, [debouncedAutosize, debouncedSaveDraft]); + }, [debouncedAutosize]); // Handlers for composition events, which are crucial for proper IME behavior const handleCompositionStart = () => { @@ -910,7 +857,6 @@ export default function ChatInput({ const canSubmit = !isLoading && - agentIsReady && (displayValue.trim() || pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) || allDroppedFiles.some((file) => !file.error && !file.isLoading)); @@ -964,11 +910,6 @@ export default function ChatInput({ setIsInGlobalHistory(false); setHasUserTyped(false); - // Clear draft when message is sent - if (chatContext && chatContext.clearDraft) { - chatContext.clearDraft(); - } - // Clear both parent and local dropped files after processing if (onFilesProcessed && droppedFiles.length > 0) { onFilesProcessed(); @@ -980,7 +921,6 @@ export default function ChatInput({ }, [ allDroppedFiles, - chatContext, displayValue, droppedFiles.length, handleSubmit, @@ -1066,7 +1006,6 @@ export default function ChatInput({ e.preventDefault(); const canSubmit = !isLoading && - agentIsReady && (displayValue.trim() || pastedImages.some((img) => img.filePath && !img.error && !img.isLoading) || allDroppedFiles.some((file) => !file.error && !file.isLoading)); @@ -1120,7 +1059,6 @@ export default function ChatInput({ isAnyDroppedFileLoading || isRecording || isTranscribing || - !agentIsReady || isExtensionsLoading; // Queue management functions - no storage persistence, only in-memory @@ -1372,7 +1310,7 @@ export default function ChatInput({ ? 'Recording...' : isTranscribing ? 'Transcribing...' - : (chatContext?.agentWaitingMessage ?? 'Send')} + : 'Send'}

diff --git a/ui/desktop/src/contexts/ChatContext.tsx b/ui/desktop/src/contexts/ChatContext.tsx index eb0537574cef..49ec41495295 100644 --- a/ui/desktop/src/contexts/ChatContext.tsx +++ b/ui/desktop/src/contexts/ChatContext.tsx @@ -1,7 +1,6 @@ import React, { createContext, useContext, ReactNode } from 'react'; import { ChatType } from '../types/chat'; import { Recipe } from '../recipe'; -import { useDraftContext } from './DraftContext'; // TODO(Douwe): We should not need this anymore export const DEFAULT_CHAT_TITLE = 'New Chat'; @@ -13,10 +12,6 @@ interface ChatContextType { hasActiveSession: boolean; setRecipe: (recipe: Recipe | null) => void; clearRecipe: () => void; - // Draft functionality - draft: string; - setDraft: (draft: string) => void; - clearDraft: () => void; // Context identification contextKey: string; // 'hub' or 'pair-{sessionId}' agentWaitingMessage: string | null; @@ -39,19 +34,6 @@ export const ChatProvider: React.FC = ({ agentWaitingMessage, contextKey = 'hub', }) => { - const draftContext = useDraftContext(); - - // Draft functionality using the app-level DraftContext - const draft = draftContext.getDraft(contextKey); - - const setDraft = (newDraft: string) => { - draftContext.setDraft(contextKey, newDraft); - }; - - const clearDraft = () => { - draftContext.clearDraft(contextKey); - }; - const resetChat = () => { setChat({ sessionId: '', @@ -61,7 +43,6 @@ export const ChatProvider: React.FC = ({ recipe: null, recipeParameterValues: null, }); - clearDraft(); }; const setRecipe = (recipe: Recipe | null) => { @@ -88,9 +69,6 @@ export const ChatProvider: React.FC = ({ hasActiveSession, setRecipe, clearRecipe, - draft, - setDraft, - clearDraft, contextKey, agentWaitingMessage, }; diff --git a/ui/desktop/src/contexts/DraftContext.tsx b/ui/desktop/src/contexts/DraftContext.tsx deleted file mode 100644 index 9238f54bfd9f..000000000000 --- a/ui/desktop/src/contexts/DraftContext.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React, { createContext, useContext, useState, ReactNode } from 'react'; - -interface DraftContextType { - getDraft: (contextKey: string) => string; - setDraft: (contextKey: string, draft: string) => void; - clearDraft: (contextKey: string) => void; -} - -const DraftContext = createContext(undefined); - -export const DraftProvider: React.FC<{ children: ReactNode }> = ({ children }) => { - // Store all drafts by contextKey - const [drafts, setDrafts] = useState>({}); - - const getDraft = (contextKey: string): string => { - return drafts[contextKey] || ''; - }; - - const setDraft = (contextKey: string, draft: string) => { - setDrafts((prev) => ({ ...prev, [contextKey]: draft })); - }; - - const clearDraft = (contextKey: string) => { - setDrafts((prev) => { - const newDrafts = { ...prev }; - delete newDrafts[contextKey]; - return newDrafts; - }); - }; - - return ( - - {children} - - ); -}; - -export const useDraftContext = (): DraftContextType => { - const context = useContext(DraftContext); - if (context === undefined) { - throw new Error('useDraftContext must be used within a DraftProvider'); - } - return context; -};