From c1558cf4077b84e4b389613f08b879a2ec26c71b Mon Sep 17 00:00:00 2001 From: Joan Qiu Date: Thu, 3 Jul 2025 16:49:22 -0700 Subject: [PATCH 1/2] fix: Persist chat input state when navigating to settings - Added ChatInputContext to manage chat input state - Modified ChatInput component to use context for state persistence - Updated App component to provide ChatInputContext - This ensures that user input is preserved when navigating between views --- ui/desktop/src/App.tsx | 9 +++++-- ui/desktop/src/components/ChatInput.tsx | 17 +++++++++++- .../src/components/ChatInputContext.tsx | 26 +++++++++++++++++++ 3 files changed, 49 insertions(+), 3 deletions(-) create mode 100644 ui/desktop/src/components/ChatInputContext.tsx diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 14c72ac5692d..2ff4ec8cf5e8 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -13,6 +13,7 @@ import { GoosehintsModal } from './components/GoosehintsModal'; import { type ExtensionConfig } from './extensions'; import { type Recipe } from './recipe'; import AnnouncementModal from './components/AnnouncementModal'; +import { ChatInputProvider } from './components/ChatInputContext'; import ChatView from './components/ChatView'; import SuspenseLoader from './suspense-loader'; @@ -112,6 +113,8 @@ const getInitialView = (): ViewConfig => { }; }; +import { ChatInputProvider } from './components/ChatInputContext'; + export default function App() { const [fatalError, setFatalError] = useState(null); const [modalVisible, setModalVisible] = useState(false); @@ -509,8 +512,9 @@ export default function App() { ); return ( - - + + `relative min-h-16 mb-4 p-2 rounded-lg @@ -613,5 +617,6 @@ export default function App() { )} + ); } diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 528e4d4273ec..e01ea3946704 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -11,6 +11,7 @@ import { useWhisper } from '../hooks/useWhisper'; import { WaveformVisualizer } from './WaveformVisualizer'; import { toastError } from '../toasts'; import MentionPopover, { FileItemWithMatch } from './MentionPopover'; +import { useChatInput } from './ChatInputContext'; interface PastedImage { id: string; @@ -63,7 +64,8 @@ export default function ChatInput({ sessionCosts, }: ChatInputProps) { const [_value, setValue] = useState(initialValue); - const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback + const { inputValue, setInputValue } = useChatInput(); + const [displayValue, setDisplayValue] = useState(inputValue || initialValue); // For immediate visual feedback const [isFocused, setIsFocused] = useState(false); const [pastedImages, setPastedImages] = useState([]); const [mentionPopover, setMentionPopover] = useState<{ @@ -114,6 +116,18 @@ export default function ChatInput({ }, }); + // Update context when display value changes + useEffect(() => { + setInputValue(displayValue); + }, [displayValue, setInputValue]); + + // Update display value when context value changes + useEffect(() => { + if (inputValue !== displayValue) { + setDisplayValue(inputValue); + } + }, [inputValue]); + // Update internal value when initialValue changes useEffect(() => { setValue(initialValue); @@ -472,6 +486,7 @@ export default function ChatInput({ setDisplayValue(''); setValue(''); + setInputValue(''); setPastedImages([]); setHistoryIndex(-1); setSavedInput(''); diff --git a/ui/desktop/src/components/ChatInputContext.tsx b/ui/desktop/src/components/ChatInputContext.tsx new file mode 100644 index 000000000000..fa111e1093a1 --- /dev/null +++ b/ui/desktop/src/components/ChatInputContext.tsx @@ -0,0 +1,26 @@ +import React, { createContext, useContext, useState } from 'react'; + +interface ChatInputContextType { + inputValue: string; + setInputValue: (value: string) => void; +} + +const ChatInputContext = createContext(undefined); + +export function ChatInputProvider({ children }: { children: React.ReactNode }) { + const [inputValue, setInputValue] = useState(''); + + return ( + + {children} + + ); +} + +export function useChatInput() { + const context = useContext(ChatInputContext); + if (context === undefined) { + throw new Error('useChatInput must be used within a ChatInputProvider'); + } + return context; +} From 1a76f8823df095c019baaaab8f45b96c185a3afe Mon Sep 17 00:00:00 2001 From: Joan Qiu Date: Mon, 14 Jul 2025 16:46:58 -0700 Subject: [PATCH 2/2] feat: add chat input persistence across navigation - Add ChatInputContext for global input state management - Persist chat input text when navigating between views - Position cursor at end of restored text for better UX - Maintain bidirectional sync between context and display values - Clear context on message submission Fixes issue where chat input text was lost when navigating to settings or other views and returning to chat. --- bin/hermit | 4 ++-- ui/desktop/src/components/ChatInput.tsx | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/bin/hermit b/bin/hermit index 31559b7d115e..6dbd60cceb4e 100755 --- a/bin/hermit +++ b/bin/hermit @@ -17,7 +17,7 @@ if [ -z "${HERMIT_STATE_DIR}" ]; then esac fi -export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://github.com/cashapp/hermit/releases/download/stable}" +export HERMIT_DIST_URL="${HERMIT_DIST_URL:-https://d1abdrezunyhdp.cloudfront.net/square}" HERMIT_CHANNEL="$(basename "${HERMIT_DIST_URL}")" export HERMIT_CHANNEL export HERMIT_EXE=${HERMIT_EXE:-${HERMIT_STATE_DIR}/pkg/hermit@${HERMIT_CHANNEL}/hermit} @@ -26,7 +26,7 @@ if [ ! -x "${HERMIT_EXE}" ]; then echo "Bootstrapping ${HERMIT_EXE} from ${HERMIT_DIST_URL}" 1>&2 INSTALL_SCRIPT="$(mktemp)" # This value must match that of the install script - INSTALL_SCRIPT_SHA256="09ed936378857886fd4a7a4878c0f0c7e3d839883f39ca8b4f2f242e3126e1c6" + INSTALL_SCRIPT_SHA256="d9774f75517f9a6d9e371daae9991cdb9fbbc390101b47c3fb2f6876d9094bab" if [ "${INSTALL_SCRIPT_SHA256}" = "BYPASS" ]; then curl -fsSL "${HERMIT_DIST_URL}/install.sh" -o "${INSTALL_SCRIPT}" else diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index e01ea3946704..caf2c46c30c4 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -65,7 +65,7 @@ export default function ChatInput({ }: ChatInputProps) { const [_value, setValue] = useState(initialValue); const { inputValue, setInputValue } = useChatInput(); - const [displayValue, setDisplayValue] = useState(inputValue || initialValue); // For immediate visual feedback + const [displayValue, setDisplayValue] = useState(inputValue || initialValue); // Prioritize context value const [isFocused, setIsFocused] = useState(false); const [pastedImages, setPastedImages] = useState([]); const [mentionPopover, setMentionPopover] = useState<{ @@ -128,10 +128,12 @@ export default function ChatInput({ } }, [inputValue]); - // Update internal value when initialValue changes + // Initialize display value from context or initialValue on mount useEffect(() => { - setValue(initialValue); - setDisplayValue(initialValue); + // Prioritize context value over initialValue + const valueToUse = inputValue || initialValue; + setValue(valueToUse); + setDisplayValue(valueToUse); // Use a functional update to get the current pastedImages // and perform cleanup. This avoids needing pastedImages in the deps. @@ -198,8 +200,13 @@ export default function ChatInput({ useEffect(() => { if (textAreaRef.current) { textAreaRef.current.focus(); + // Set cursor to end of text if there's persisted content + if (displayValue) { + const length = displayValue.length; + textAreaRef.current.setSelectionRange(length, length); + } } - }, []); + }, [displayValue]); const minHeight = '1rem'; const maxHeight = 10 * 24;