Disable chat input while extensions load (#4417)

This commit is contained in:
Amed Rodriguez
2025-09-02 14:03:20 -07:00
committed by GitHub
parent d6dbbab8bc
commit 4e9c5e3e2d
8 changed files with 105 additions and 52 deletions
+16 -10
View File
@@ -45,11 +45,13 @@ const HubRouteWrapper = ({
setChat, setChat,
setPairChat, setPairChat,
setIsGoosehintsModalOpen, setIsGoosehintsModalOpen,
isExtensionsLoading,
}: { }: {
chat: ChatType; chat: ChatType;
setChat: (chat: ChatType) => void; setChat: (chat: ChatType) => void;
setPairChat: (chat: ChatType) => void; setPairChat: (chat: ChatType) => void;
setIsGoosehintsModalOpen: (isOpen: boolean) => void; setIsGoosehintsModalOpen: (isOpen: boolean) => void;
isExtensionsLoading: boolean;
}) => { }) => {
const navigate = useNavigate(); const navigate = useNavigate();
const setView = createNavigationHandler(navigate); const setView = createNavigationHandler(navigate);
@@ -62,6 +64,7 @@ const HubRouteWrapper = ({
setPairChat={setPairChat} setPairChat={setPairChat}
setView={setView} setView={setView}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
isExtensionsLoading={isExtensionsLoading}
/> />
); );
}; };
@@ -400,6 +403,7 @@ export default function App() {
const [agentWaitingMessage, setAgentWaitingMessage] = useState<string | null>(null); const [agentWaitingMessage, setAgentWaitingMessage] = useState<string | null>(null);
const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false); const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false);
const [sharedSessionError, setSharedSessionError] = useState<string | null>(null); const [sharedSessionError, setSharedSessionError] = useState<string | null>(null);
const [isExtensionsLoading, setIsExtensionsLoading] = useState(false);
// Add separate state for pair chat to maintain its own conversation // Add separate state for pair chat to maintain its own conversation
const [pairChat, setPairChat] = useState<ChatType>({ const [pairChat, setPairChat] = useState<ChatType>({
@@ -482,6 +486,7 @@ export default function App() {
addExtension, addExtension,
setPairChat, setPairChat,
setMessage: setAgentWaitingMessage, setMessage: setAgentWaitingMessage,
setIsExtensionsLoading,
provider: provider as string, provider: provider as string,
model: model as string, model: model as string,
}); });
@@ -802,12 +807,13 @@ export default function App() {
<Route <Route
index index
element={ element={
<ProviderGuard> <ProviderGuard setIsExtensionsLoading={setIsExtensionsLoading}>
<HubRouteWrapper <HubRouteWrapper
chat={chat} chat={chat}
setChat={setChat} setChat={setChat}
setPairChat={setPairChat} setPairChat={setPairChat}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
isExtensionsLoading={isExtensionsLoading}
/> />
</ProviderGuard> </ProviderGuard>
} }
@@ -815,7 +821,7 @@ export default function App() {
<Route <Route
path="pair" path="pair"
element={ element={
<ProviderGuard> <ProviderGuard setIsExtensionsLoading={setIsExtensionsLoading}>
<ChatProvider <ChatProvider
chat={pairChat} chat={pairChat}
setChat={setPairChat} setChat={setPairChat}
@@ -836,7 +842,7 @@ export default function App() {
<Route <Route
path="settings" path="settings"
element={ element={
<ProviderGuard> <ProviderGuard setIsExtensionsLoading={setIsExtensionsLoading}>
<SettingsRoute /> <SettingsRoute />
</ProviderGuard> </ProviderGuard>
} }
@@ -844,7 +850,7 @@ export default function App() {
<Route <Route
path="extensions" path="extensions"
element={ element={
<ProviderGuard> <ProviderGuard setIsExtensionsLoading={setIsExtensionsLoading}>
<ExtensionsRoute /> <ExtensionsRoute />
</ProviderGuard> </ProviderGuard>
} }
@@ -852,7 +858,7 @@ export default function App() {
<Route <Route
path="sessions" path="sessions"
element={ element={
<ProviderGuard> <ProviderGuard setIsExtensionsLoading={setIsExtensionsLoading}>
<SessionsRoute /> <SessionsRoute />
</ProviderGuard> </ProviderGuard>
} }
@@ -860,7 +866,7 @@ export default function App() {
<Route <Route
path="schedules" path="schedules"
element={ element={
<ProviderGuard> <ProviderGuard setIsExtensionsLoading={setIsExtensionsLoading}>
<SchedulesRoute /> <SchedulesRoute />
</ProviderGuard> </ProviderGuard>
} }
@@ -868,7 +874,7 @@ export default function App() {
<Route <Route
path="recipes" path="recipes"
element={ element={
<ProviderGuard> <ProviderGuard setIsExtensionsLoading={setIsExtensionsLoading}>
<RecipesRoute /> <RecipesRoute />
</ProviderGuard> </ProviderGuard>
} }
@@ -876,7 +882,7 @@ export default function App() {
<Route <Route
path="recipe-editor" path="recipe-editor"
element={ element={
<ProviderGuard> <ProviderGuard setIsExtensionsLoading={setIsExtensionsLoading}>
<RecipeEditorRoute /> <RecipeEditorRoute />
</ProviderGuard> </ProviderGuard>
} }
@@ -884,7 +890,7 @@ export default function App() {
<Route <Route
path="shared-session" path="shared-session"
element={ element={
<ProviderGuard> <ProviderGuard setIsExtensionsLoading={setIsExtensionsLoading}>
<SharedSessionRouteWrapper <SharedSessionRouteWrapper
isLoadingSharedSession={isLoadingSharedSession} isLoadingSharedSession={isLoadingSharedSession}
setIsLoadingSharedSession={setIsLoadingSharedSession} setIsLoadingSharedSession={setIsLoadingSharedSession}
@@ -896,7 +902,7 @@ export default function App() {
<Route <Route
path="permission" path="permission"
element={ element={
<ProviderGuard> <ProviderGuard setIsExtensionsLoading={setIsExtensionsLoading}>
<PermissionRoute /> <PermissionRoute />
</ProviderGuard> </ProviderGuard>
} }
+37 -27
View File
@@ -86,6 +86,7 @@ interface ChatInputProps {
autoSubmit: boolean; autoSubmit: boolean;
setAncestorMessages?: (messages: Message[]) => void; setAncestorMessages?: (messages: Message[]) => void;
append?: (message: Message) => void; append?: (message: Message) => void;
isExtensionsLoading?: boolean;
} }
export default function ChatInput({ export default function ChatInput({
@@ -111,6 +112,7 @@ export default function ChatInput({
autoSubmit = false, autoSubmit = false,
append, append,
setAncestorMessages, setAncestorMessages,
isExtensionsLoading = false,
}: ChatInputProps) { }: ChatInputProps) {
const [_value, setValue] = useState(initialValue); const [_value, setValue] = useState(initialValue);
const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback const [displayValue, setDisplayValue] = useState(initialValue); // For immediate visual feedback
@@ -1125,6 +1127,25 @@ export default function ChatInput({
const isAnyImageLoading = pastedImages.some((img) => img.isLoading); const isAnyImageLoading = pastedImages.some((img) => img.isLoading);
const isAnyDroppedFileLoading = allDroppedFiles.some((file) => file.isLoading); const isAnyDroppedFileLoading = allDroppedFiles.some((file) => file.isLoading);
const isSubmitButtonDisabled =
!hasSubmittableContent ||
isAnyImageLoading ||
isAnyDroppedFileLoading ||
isRecording ||
isTranscribing ||
isCompacting ||
!agentIsReady ||
isExtensionsLoading;
const isUserInputDisabled =
isAnyImageLoading ||
isAnyDroppedFileLoading ||
isRecording ||
isTranscribing ||
isCompacting ||
!agentIsReady ||
isExtensionsLoading;
// Queue management functions - no storage persistence, only in-memory // Queue management functions - no storage persistence, only in-memory
const handleRemoveQueuedMessage = (messageId: string) => { const handleRemoveQueuedMessage = (messageId: string) => {
setQueuedMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setQueuedMessages((prev) => prev.filter((msg) => msg.id !== messageId));
@@ -1239,6 +1260,7 @@ export default function ChatInput({
onBlur={() => setIsFocused(false)} onBlur={() => setIsFocused(false)}
ref={textAreaRef} ref={textAreaRef}
rows={1} rows={1}
disabled={isUserInputDisabled}
style={{ style={{
maxHeight: `${maxHeight}px`, maxHeight: `${maxHeight}px`,
overflowY: 'auto', overflowY: 'auto',
@@ -1349,23 +1371,9 @@ export default function ChatInput({
size="sm" size="sm"
shape="round" shape="round"
variant="outline" variant="outline"
disabled={ disabled={isSubmitButtonDisabled}
!hasSubmittableContent ||
isAnyImageLoading ||
isAnyDroppedFileLoading ||
isRecording ||
isTranscribing ||
isCompacting ||
!agentIsReady
}
className={`rounded-full px-10 py-2 flex items-center gap-2 ${ className={`rounded-full px-10 py-2 flex items-center gap-2 ${
!hasSubmittableContent || isSubmitButtonDisabled
isAnyImageLoading ||
isAnyDroppedFileLoading ||
isRecording ||
isTranscribing ||
isCompacting ||
!agentIsReady
? 'bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600' ? 'bg-slate-600 text-white cursor-not-allowed opacity-50 border-slate-600'
: 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600 hover:cursor-pointer' : 'bg-slate-600 text-white hover:bg-slate-700 border-slate-600 hover:cursor-pointer'
}`} }`}
@@ -1377,17 +1385,19 @@ export default function ChatInput({
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
<p> <p>
{isCompacting {isExtensionsLoading
? 'Compacting conversation...' ? 'Loading extensions...'
: isAnyImageLoading : isCompacting
? 'Waiting for images to save...' ? 'Compacting conversation...'
: isAnyDroppedFileLoading : isAnyImageLoading
? 'Processing dropped files...' ? 'Waiting for images to save...'
: isRecording : isAnyDroppedFileLoading
? 'Recording...' ? 'Processing dropped files...'
: isTranscribing : isRecording
? 'Transcribing...' ? 'Recording...'
: (chatContext?.agentWaitingMessage ?? 'Send')} : isTranscribing
? 'Transcribing...'
: (chatContext?.agentWaitingMessage ?? 'Send')}
</p> </p>
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
+11 -9
View File
@@ -16,9 +16,10 @@ import { Ollama } from './icons';
interface OllamaSetupProps { interface OllamaSetupProps {
onSuccess: () => void; onSuccess: () => void;
onCancel: () => void; onCancel: () => void;
setIsExtensionsLoading?: (loading: boolean) => void;
} }
export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { export function OllamaSetup({ onSuccess, onCancel, setIsExtensionsLoading }: OllamaSetupProps) {
const { addExtension, getExtensions, upsert } = useConfig(); const { addExtension, getExtensions, upsert } = useConfig();
const [isChecking, setIsChecking] = useState(true); const [isChecking, setIsChecking] = useState(true);
const [ollamaDetected, setOllamaDetected] = useState(false); const [ollamaDetected, setOllamaDetected] = useState(false);
@@ -113,6 +114,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
await initializeSystem('ollama', getPreferredModel(), { await initializeSystem('ollama', getPreferredModel(), {
getExtensions, getExtensions,
addExtension, addExtension,
setIsExtensionsLoading,
}); });
toastService.success({ toastService.success({
@@ -157,7 +159,9 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
{ollamaDetected ? ( {ollamaDetected ? (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-start mb-16"> <div className="flex items-start mb-16">
<span className="inline-block px-2 py-1 text-xs font-medium bg-green-600 text-white rounded-full">Ollama is detected and running</span> <span className="inline-block px-2 py-1 text-xs font-medium bg-green-600 text-white rounded-full">
Ollama is detected and running
</span>
</div> </div>
{modelStatus === 'checking' ? ( {modelStatus === 'checking' ? (
@@ -185,14 +189,10 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
) : modelStatus === 'downloading' ? ( ) : modelStatus === 'downloading' ? (
<div className="space-y-4"> <div className="space-y-4">
<div className="bg-background-info/10 border border-border-info rounded-lg p-4"> <div className="bg-background-info/10 border border-border-info rounded-lg p-4">
<p className="text-text-info text-sm"> <p className="text-text-info text-sm">Downloading {getPreferredModel()}...</p>
Downloading {getPreferredModel()}...
</p>
{downloadProgress && ( {downloadProgress && (
<> <>
<p className="text-text-muted text-xs mt-2"> <p className="text-text-muted text-xs mt-2">{downloadProgress.status}</p>
{downloadProgress.status}
</p>
{downloadProgress.total && downloadProgress.completed && ( {downloadProgress.total && downloadProgress.completed && (
<div className="mt-3"> <div className="mt-3">
<div className="bg-background-muted rounded-full h-2 overflow-hidden"> <div className="bg-background-muted rounded-full h-2 overflow-hidden">
@@ -225,7 +225,9 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
) : ( ) : (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-start mb-16"> <div className="flex items-start mb-16">
<span className="inline-block px-2 py-1 text-xs font-medium bg-orange-600 text-white rounded-full">Ollama is not detected on your system</span> <span className="inline-block px-2 py-1 text-xs font-medium bg-orange-600 text-white rounded-full">
Ollama is not detected on your system
</span>
</div> </div>
{isPolling ? ( {isPolling ? (
+5 -1
View File
@@ -14,9 +14,10 @@ import { OpenRouter } from './icons';
interface ProviderGuardProps { interface ProviderGuardProps {
children: React.ReactNode; children: React.ReactNode;
setIsExtensionsLoading?: (loading: boolean) => void;
} }
export default function ProviderGuard({ children }: ProviderGuardProps) { export default function ProviderGuard({ children, setIsExtensionsLoading }: ProviderGuardProps) {
const { read, getExtensions, addExtension } = useConfig(); const { read, getExtensions, addExtension } = useConfig();
const navigate = useNavigate(); const navigate = useNavigate();
const [isChecking, setIsChecking] = useState(true); const [isChecking, setIsChecking] = useState(true);
@@ -72,6 +73,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
await initializeSystem(provider as string, model as string, { await initializeSystem(provider as string, model as string, {
getExtensions, getExtensions,
addExtension, addExtension,
setIsExtensionsLoading,
}); });
toastService.configure({ silent: false }); toastService.configure({ silent: false });
@@ -138,6 +140,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
await initializeSystem(provider as string, model as string, { await initializeSystem(provider as string, model as string, {
getExtensions, getExtensions,
addExtension, addExtension,
setIsExtensionsLoading,
}); });
toastService.configure({ silent: false }); toastService.configure({ silent: false });
@@ -267,6 +270,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
setShowOllamaSetup(false); setShowOllamaSetup(false);
setShowFirstTimeSetup(true); setShowFirstTimeSetup(true);
}} }}
setIsExtensionsLoading={setIsExtensionsLoading}
/> />
</div> </div>
</div> </div>
+3
View File
@@ -36,6 +36,7 @@ export default function Hub({
setPairChat, setPairChat,
setView, setView,
setIsGoosehintsModalOpen, setIsGoosehintsModalOpen,
isExtensionsLoading,
}: { }: {
readyForAutoUserPrompt: boolean; readyForAutoUserPrompt: boolean;
chat: ChatType; chat: ChatType;
@@ -43,6 +44,7 @@ export default function Hub({
setPairChat: (chat: ChatType) => void; setPairChat: (chat: ChatType) => void;
setView: (view: View, viewOptions?: ViewOptions) => void; setView: (view: View, viewOptions?: ViewOptions) => void;
setIsGoosehintsModalOpen: (isOpen: boolean) => void; setIsGoosehintsModalOpen: (isOpen: boolean) => void;
isExtensionsLoading: boolean;
}) { }) {
// Handle chat input submission - create new chat and navigate to pair // Handle chat input submission - create new chat and navigate to pair
const handleSubmit = (e: React.FormEvent) => { const handleSubmit = (e: React.FormEvent) => {
@@ -101,6 +103,7 @@ export default function Hub({
disableAnimation={false} disableAnimation={false}
sessionCosts={undefined} sessionCosts={undefined}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen}
isExtensionsLoading={isExtensionsLoading}
/> />
</div> </div>
</ContextManagerProvider> </ContextManagerProvider>
@@ -10,9 +10,14 @@ import { toastService } from '../../../toasts';
interface ProviderSettingsProps { interface ProviderSettingsProps {
onClose: () => void; onClose: () => void;
isOnboarding: boolean; isOnboarding: boolean;
setIsExtensionsLoading?: (loading: boolean) => void;
} }
export default function ProviderSettings({ onClose, isOnboarding }: ProviderSettingsProps) { export default function ProviderSettings({
onClose,
isOnboarding,
setIsExtensionsLoading,
}: ProviderSettingsProps) {
const { getProviders, upsert, getExtensions, addExtension } = useConfig(); const { getProviders, upsert, getExtensions, addExtension } = useConfig();
const [loading, setLoading] = useState(true); const [loading, setLoading] = useState(true);
const [providers, setProviders] = useState<ProviderDetails[]>([]); const [providers, setProviders] = useState<ProviderDetails[]>([]);
@@ -71,6 +76,7 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett
await initializeSystem(provider.name, model, { await initializeSystem(provider.name, model, {
getExtensions, getExtensions,
addExtension, addExtension,
setIsExtensionsLoading,
}); });
toastService.configure({ silent: false }); toastService.configure({ silent: false });
@@ -92,7 +98,7 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett
}); });
} }
}, },
[onClose, upsert, getExtensions, addExtension] [onClose, upsert, getExtensions, addExtension, setIsExtensionsLoading]
); );
return ( return (
+20 -3
View File
@@ -15,6 +15,7 @@ interface InitializationDependencies {
addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>; addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
setPairChat: (chat: ChatType | ((prev: ChatType) => ChatType)) => void; setPairChat: (chat: ChatType | ((prev: ChatType) => ChatType)) => void;
setMessage: (message: string | null) => void; setMessage: (message: string | null) => void;
setIsExtensionsLoading: (loading: boolean) => void;
provider: string; provider: string;
model: string; model: string;
} }
@@ -24,6 +25,7 @@ export const initializeApp = async ({
addExtension, addExtension,
setPairChat, setPairChat,
setMessage, setMessage,
setIsExtensionsLoading,
provider, provider,
model, model,
}: InitializationDependencies) => { }: InitializationDependencies) => {
@@ -36,7 +38,13 @@ export const initializeApp = async ({
if (resumeSessionId) { if (resumeSessionId) {
console.log('Session resume detected, letting useChat hook handle navigation'); console.log('Session resume detected, letting useChat hook handle navigation');
await initializeForSessionResume({ getExtensions, addExtension, provider, model }); await initializeForSessionResume({
getExtensions,
addExtension,
setIsExtensionsLoading,
provider,
model,
});
return; return;
} }
@@ -47,6 +55,7 @@ export const initializeApp = async ({
getExtensions, getExtensions,
addExtension, addExtension,
setPairChat, setPairChat,
setIsExtensionsLoading,
provider, provider,
model, model,
}); });
@@ -82,6 +91,7 @@ export const initializeApp = async ({
initializeSystem(provider, model, { initializeSystem(provider, model, {
getExtensions, getExtensions,
addExtension, addExtension,
setIsExtensionsLoading,
}), }),
]; ];
@@ -115,15 +125,20 @@ export const initializeApp = async ({
const initializeForSessionResume = async ({ const initializeForSessionResume = async ({
getExtensions, getExtensions,
addExtension, addExtension,
setIsExtensionsLoading,
provider, provider,
model, model,
}: Pick<InitializationDependencies, 'getExtensions' | 'addExtension' | 'provider' | 'model'>) => { }: Pick<
InitializationDependencies,
'getExtensions' | 'addExtension' | 'setIsExtensionsLoading' | 'provider' | 'model'
>) => {
await initConfig(); await initConfig();
await readAllConfig({ throwOnError: true }); await readAllConfig({ throwOnError: true });
await initializeSystem(provider, model, { await initializeSystem(provider, model, {
getExtensions, getExtensions,
addExtension, addExtension,
setIsExtensionsLoading,
}); });
}; };
@@ -132,11 +147,12 @@ const initializeForRecipe = async ({
getExtensions, getExtensions,
addExtension, addExtension,
setPairChat, setPairChat,
setIsExtensionsLoading,
provider, provider,
model, model,
}: Pick< }: Pick<
InitializationDependencies, InitializationDependencies,
'getExtensions' | 'addExtension' | 'setPairChat' | 'provider' | 'model' 'getExtensions' | 'addExtension' | 'setPairChat' | 'setIsExtensionsLoading' | 'provider' | 'model'
> & { > & {
recipeConfig: Recipe; recipeConfig: Recipe;
}) => { }) => {
@@ -146,6 +162,7 @@ const initializeForRecipe = async ({
await initializeSystem(provider, model, { await initializeSystem(provider, model, {
getExtensions, getExtensions,
addExtension, addExtension,
setIsExtensionsLoading,
}); });
setPairChat((prevChat) => ({ setPairChat((prevChat) => ({
+5
View File
@@ -115,6 +115,7 @@ export const initializeSystem = async (
options?: { options?: {
getExtensions?: (b: boolean) => Promise<FixedExtensionEntry[]>; getExtensions?: (b: boolean) => Promise<FixedExtensionEntry[]>;
addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>; addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>;
setIsExtensionsLoading?: (loading: boolean) => void;
} }
) => { ) => {
try { try {
@@ -182,6 +183,8 @@ export const initializeSystem = async (
// Add enabled extensions to agent in parallel // Add enabled extensions to agent in parallel
const enabledExtensions = refreshedExtensions.filter((ext) => ext.enabled); const enabledExtensions = refreshedExtensions.filter((ext) => ext.enabled);
options?.setIsExtensionsLoading?.(true);
const extensionLoadingPromises = enabledExtensions.map(async (extensionEntry) => { const extensionLoadingPromises = enabledExtensions.map(async (extensionEntry) => {
const extensionConfig = extractExtensionConfig(extensionEntry); const extensionConfig = extractExtensionConfig(extensionEntry);
const extensionName = extensionConfig.name; const extensionName = extensionConfig.name;
@@ -198,8 +201,10 @@ export const initializeSystem = async (
}); });
await Promise.allSettled(extensionLoadingPromises); await Promise.allSettled(extensionLoadingPromises);
options?.setIsExtensionsLoading?.(false);
} catch (error) { } catch (error) {
console.error('Failed to initialize agent:', error); console.error('Failed to initialize agent:', error);
options?.setIsExtensionsLoading?.(false);
throw error; throw error;
} }
}; };