diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 2b1e7381eb1e..33b00b141eea 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -45,11 +45,13 @@ const HubRouteWrapper = ({ setChat, setPairChat, setIsGoosehintsModalOpen, + isExtensionsLoading, }: { chat: ChatType; setChat: (chat: ChatType) => void; setPairChat: (chat: ChatType) => void; setIsGoosehintsModalOpen: (isOpen: boolean) => void; + isExtensionsLoading: boolean; }) => { const navigate = useNavigate(); const setView = createNavigationHandler(navigate); @@ -62,6 +64,7 @@ const HubRouteWrapper = ({ setPairChat={setPairChat} setView={setView} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} + isExtensionsLoading={isExtensionsLoading} /> ); }; @@ -400,6 +403,7 @@ export default function App() { const [agentWaitingMessage, setAgentWaitingMessage] = useState(null); const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false); const [sharedSessionError, setSharedSessionError] = useState(null); + const [isExtensionsLoading, setIsExtensionsLoading] = useState(false); // Add separate state for pair chat to maintain its own conversation const [pairChat, setPairChat] = useState({ @@ -482,6 +486,7 @@ export default function App() { addExtension, setPairChat, setMessage: setAgentWaitingMessage, + setIsExtensionsLoading, provider: provider as string, model: model as string, }); @@ -802,12 +807,13 @@ export default function App() { + } @@ -815,7 +821,7 @@ export default function App() { + + } @@ -844,7 +850,7 @@ export default function App() { + } @@ -852,7 +858,7 @@ export default function App() { + } @@ -860,7 +866,7 @@ export default function App() { + } @@ -868,7 +874,7 @@ export default function App() { + } @@ -876,7 +882,7 @@ export default function App() { + } @@ -884,7 +890,7 @@ export default function App() { + + } diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index dba2f0080046..16a9db65f805 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -86,6 +86,7 @@ interface ChatInputProps { autoSubmit: boolean; setAncestorMessages?: (messages: Message[]) => void; append?: (message: Message) => void; + isExtensionsLoading?: boolean; } export default function ChatInput({ @@ -111,6 +112,7 @@ export default function ChatInput({ autoSubmit = false, append, setAncestorMessages, + isExtensionsLoading = false, }: ChatInputProps) { const [_value, setValue] = useState(initialValue); 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 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 const handleRemoveQueuedMessage = (messageId: string) => { setQueuedMessages((prev) => prev.filter((msg) => msg.id !== messageId)); @@ -1239,6 +1260,7 @@ export default function ChatInput({ onBlur={() => setIsFocused(false)} ref={textAreaRef} rows={1} + disabled={isUserInputDisabled} style={{ maxHeight: `${maxHeight}px`, overflowY: 'auto', @@ -1349,23 +1371,9 @@ export default function ChatInput({ size="sm" shape="round" variant="outline" - disabled={ - !hasSubmittableContent || - isAnyImageLoading || - isAnyDroppedFileLoading || - isRecording || - isTranscribing || - isCompacting || - !agentIsReady - } + disabled={isSubmitButtonDisabled} className={`rounded-full px-10 py-2 flex items-center gap-2 ${ - !hasSubmittableContent || - isAnyImageLoading || - isAnyDroppedFileLoading || - isRecording || - isTranscribing || - isCompacting || - !agentIsReady + isSubmitButtonDisabled ? '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' }`} @@ -1377,17 +1385,19 @@ export default function ChatInput({

- {isCompacting - ? 'Compacting conversation...' - : isAnyImageLoading - ? 'Waiting for images to save...' - : isAnyDroppedFileLoading - ? 'Processing dropped files...' - : isRecording - ? 'Recording...' - : isTranscribing - ? 'Transcribing...' - : (chatContext?.agentWaitingMessage ?? 'Send')} + {isExtensionsLoading + ? 'Loading extensions...' + : isCompacting + ? 'Compacting conversation...' + : isAnyImageLoading + ? 'Waiting for images to save...' + : isAnyDroppedFileLoading + ? 'Processing dropped files...' + : isRecording + ? 'Recording...' + : isTranscribing + ? 'Transcribing...' + : (chatContext?.agentWaitingMessage ?? 'Send')}

diff --git a/ui/desktop/src/components/OllamaSetup.tsx b/ui/desktop/src/components/OllamaSetup.tsx index 2d104d0f9632..9f54f0b1353e 100644 --- a/ui/desktop/src/components/OllamaSetup.tsx +++ b/ui/desktop/src/components/OllamaSetup.tsx @@ -16,9 +16,10 @@ import { Ollama } from './icons'; interface OllamaSetupProps { onSuccess: () => 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 [isChecking, setIsChecking] = useState(true); const [ollamaDetected, setOllamaDetected] = useState(false); @@ -113,6 +114,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { await initializeSystem('ollama', getPreferredModel(), { getExtensions, addExtension, + setIsExtensionsLoading, }); toastService.success({ @@ -157,7 +159,9 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { {ollamaDetected ? (
- Ollama is detected and running + + Ollama is detected and running +
{modelStatus === 'checking' ? ( @@ -185,14 +189,10 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { ) : modelStatus === 'downloading' ? (
-

- Downloading {getPreferredModel()}... -

+

Downloading {getPreferredModel()}...

{downloadProgress && ( <> -

- {downloadProgress.status} -

+

{downloadProgress.status}

{downloadProgress.total && downloadProgress.completed && (
@@ -225,7 +225,9 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { ) : (
- Ollama is not detected on your system + + Ollama is not detected on your system +
{isPolling ? ( diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 89fb8e5ce13d..fc821c15eff6 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -14,9 +14,10 @@ import { OpenRouter } from './icons'; interface ProviderGuardProps { 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 navigate = useNavigate(); const [isChecking, setIsChecking] = useState(true); @@ -72,6 +73,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { await initializeSystem(provider as string, model as string, { getExtensions, addExtension, + setIsExtensionsLoading, }); toastService.configure({ silent: false }); @@ -138,6 +140,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { await initializeSystem(provider as string, model as string, { getExtensions, addExtension, + setIsExtensionsLoading, }); toastService.configure({ silent: false }); @@ -267,6 +270,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { setShowOllamaSetup(false); setShowFirstTimeSetup(true); }} + setIsExtensionsLoading={setIsExtensionsLoading} />
diff --git a/ui/desktop/src/components/hub.tsx b/ui/desktop/src/components/hub.tsx index 732278e244f9..116ddf8b3b94 100644 --- a/ui/desktop/src/components/hub.tsx +++ b/ui/desktop/src/components/hub.tsx @@ -36,6 +36,7 @@ export default function Hub({ setPairChat, setView, setIsGoosehintsModalOpen, + isExtensionsLoading, }: { readyForAutoUserPrompt: boolean; chat: ChatType; @@ -43,6 +44,7 @@ export default function Hub({ setPairChat: (chat: ChatType) => void; setView: (view: View, viewOptions?: ViewOptions) => void; setIsGoosehintsModalOpen: (isOpen: boolean) => void; + isExtensionsLoading: boolean; }) { // Handle chat input submission - create new chat and navigate to pair const handleSubmit = (e: React.FormEvent) => { @@ -101,6 +103,7 @@ export default function Hub({ disableAnimation={false} sessionCosts={undefined} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} + isExtensionsLoading={isExtensionsLoading} />
diff --git a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx index dd2b9c81036b..0e5b52672866 100644 --- a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx @@ -10,9 +10,14 @@ import { toastService } from '../../../toasts'; interface ProviderSettingsProps { onClose: () => void; 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 [loading, setLoading] = useState(true); const [providers, setProviders] = useState([]); @@ -71,6 +76,7 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett await initializeSystem(provider.name, model, { getExtensions, addExtension, + setIsExtensionsLoading, }); 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 ( diff --git a/ui/desktop/src/utils/appInitialization.ts b/ui/desktop/src/utils/appInitialization.ts index 054142036cac..f311d5886fe9 100644 --- a/ui/desktop/src/utils/appInitialization.ts +++ b/ui/desktop/src/utils/appInitialization.ts @@ -15,6 +15,7 @@ interface InitializationDependencies { addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise; setPairChat: (chat: ChatType | ((prev: ChatType) => ChatType)) => void; setMessage: (message: string | null) => void; + setIsExtensionsLoading: (loading: boolean) => void; provider: string; model: string; } @@ -24,6 +25,7 @@ export const initializeApp = async ({ addExtension, setPairChat, setMessage, + setIsExtensionsLoading, provider, model, }: InitializationDependencies) => { @@ -36,7 +38,13 @@ export const initializeApp = async ({ if (resumeSessionId) { console.log('Session resume detected, letting useChat hook handle navigation'); - await initializeForSessionResume({ getExtensions, addExtension, provider, model }); + await initializeForSessionResume({ + getExtensions, + addExtension, + setIsExtensionsLoading, + provider, + model, + }); return; } @@ -47,6 +55,7 @@ export const initializeApp = async ({ getExtensions, addExtension, setPairChat, + setIsExtensionsLoading, provider, model, }); @@ -82,6 +91,7 @@ export const initializeApp = async ({ initializeSystem(provider, model, { getExtensions, addExtension, + setIsExtensionsLoading, }), ]; @@ -115,15 +125,20 @@ export const initializeApp = async ({ const initializeForSessionResume = async ({ getExtensions, addExtension, + setIsExtensionsLoading, provider, model, -}: Pick) => { +}: Pick< + InitializationDependencies, + 'getExtensions' | 'addExtension' | 'setIsExtensionsLoading' | 'provider' | 'model' +>) => { await initConfig(); await readAllConfig({ throwOnError: true }); await initializeSystem(provider, model, { getExtensions, addExtension, + setIsExtensionsLoading, }); }; @@ -132,11 +147,12 @@ const initializeForRecipe = async ({ getExtensions, addExtension, setPairChat, + setIsExtensionsLoading, provider, model, }: Pick< InitializationDependencies, - 'getExtensions' | 'addExtension' | 'setPairChat' | 'provider' | 'model' + 'getExtensions' | 'addExtension' | 'setPairChat' | 'setIsExtensionsLoading' | 'provider' | 'model' > & { recipeConfig: Recipe; }) => { @@ -146,6 +162,7 @@ const initializeForRecipe = async ({ await initializeSystem(provider, model, { getExtensions, addExtension, + setIsExtensionsLoading, }); setPairChat((prevChat) => ({ diff --git a/ui/desktop/src/utils/providerUtils.ts b/ui/desktop/src/utils/providerUtils.ts index cdcbac182c28..e86eb220afc5 100644 --- a/ui/desktop/src/utils/providerUtils.ts +++ b/ui/desktop/src/utils/providerUtils.ts @@ -115,6 +115,7 @@ export const initializeSystem = async ( options?: { getExtensions?: (b: boolean) => Promise; addExtension?: (name: string, config: ExtensionConfig, enabled: boolean) => Promise; + setIsExtensionsLoading?: (loading: boolean) => void; } ) => { try { @@ -182,6 +183,8 @@ export const initializeSystem = async ( // Add enabled extensions to agent in parallel const enabledExtensions = refreshedExtensions.filter((ext) => ext.enabled); + options?.setIsExtensionsLoading?.(true); + const extensionLoadingPromises = enabledExtensions.map(async (extensionEntry) => { const extensionConfig = extractExtensionConfig(extensionEntry); const extensionName = extensionConfig.name; @@ -198,8 +201,10 @@ export const initializeSystem = async ( }); await Promise.allSettled(extensionLoadingPromises); + options?.setIsExtensionsLoading?.(false); } catch (error) { console.error('Failed to initialize agent:', error); + options?.setIsExtensionsLoading?.(false); throw error; } };