diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx index 4674cc911ac7..fa3121b6e3b0 100644 --- a/ui/desktop/src/App.tsx +++ b/ui/desktop/src/App.tsx @@ -8,6 +8,7 @@ import { ToastContainer } from 'react-toastify'; import { toastService } from './toasts'; import { extractExtensionName } from './components/settings/extensions/utils'; import { GoosehintsModal } from './components/GoosehintsModal'; +import { type ExtensionConfig } from './extensions'; import ChatView from './components/ChatView'; import SuspenseLoader from './suspense-loader'; @@ -46,10 +47,28 @@ export type View = | 'recipeEditor' | 'permission'; -export type ViewOptions = - | SettingsViewOptions - | { resumedSession?: SessionDetails } - | Record; +export type ViewOptions = { + // Settings view options + extensionId?: string; + showEnvVars?: boolean; + deepLinkConfig?: ExtensionConfig; + + // Session view options + resumedSession?: SessionDetails; + sessionDetails?: SessionDetails; + error?: string; + shareToken?: string; + baseUrl?: string; + + // Recipe editor options + config?: unknown; + + // Permission view options + parentView?: View; + + // Generic options + [key: string]: unknown; +}; export type ViewConfig = { view: View; @@ -231,9 +250,9 @@ export default function App() { setIsLoadingSharedSession(false); } }; - window.electron.on('open-shared-session', handleOpenSharedSession as any); + window.electron.on('open-shared-session', handleOpenSharedSession); return () => { - window.electron.off('open-shared-session', handleOpenSharedSession as any); + window.electron.off('open-shared-session', handleOpenSharedSession); }; }, []); @@ -266,9 +285,9 @@ export default function App() { console.error('Is loading session:', isLoadingSession); setFatalError(errorMessage); }; - window.electron.on('fatal-error', handleFatalError as any); + window.electron.on('fatal-error', handleFatalError); return () => { - window.electron.off('fatal-error', handleFatalError as any); + window.electron.off('fatal-error', handleFatalError); }; }, [view, isLoadingSession]); @@ -292,8 +311,8 @@ export default function App() { setView(viewFromUrl as View); } } - window.electron.on('set-view', handleSetView as any); - return () => window.electron.off('set-view', handleSetView as any); + window.electron.on('set-view', handleSetView); + return () => window.electron.off('set-view', handleSetView); }, []); useEffect(() => { @@ -375,9 +394,9 @@ export default function App() { console.error('Error handling add-extension event:', error); } }; - window.electron.on('add-extension', handleAddExtension as any); + window.electron.on('add-extension', handleAddExtension); return () => { - window.electron.off('add-extension', handleAddExtension as any); + window.electron.off('add-extension', handleAddExtension); }; }, [STRICT_ALLOWLIST]); @@ -388,9 +407,9 @@ export default function App() { inputField.focus(); } }; - window.electron.on('focus-input', handleFocusInput as any); + window.electron.on('focus-input', handleFocusInput); return () => { - window.electron.off('focus-input', handleFocusInput as any); + window.electron.off('focus-input', handleFocusInput); }; }, []); diff --git a/ui/desktop/src/components/ChatInput.tsx b/ui/desktop/src/components/ChatInput.tsx index 8767f9a5b077..77f4a2bb63f0 100644 --- a/ui/desktop/src/components/ChatInput.tsx +++ b/ui/desktop/src/components/ChatInput.tsx @@ -1,4 +1,4 @@ -import React, { useRef, useState, useEffect, useCallback } from 'react'; +import React, { useRef, useState, useEffect, useMemo } from 'react'; import { Button } from './ui/button'; import type { View } from '../App'; import Stop from './ui/Stop'; @@ -148,21 +148,20 @@ export default function ChatInput({ }, [droppedFiles, processedFilePaths, displayValue]); // Debounced function to update actual value - const debouncedSetValue = useCallback((val: string) => { - debounce((value: string) => { + const debouncedSetValue = useMemo( + () => debounce((value: string) => { setValue(value); - }, 150)(val); - }, []); + }, 150), + [setValue] + ); // Debounced autosize function - const debouncedAutosize = useCallback( - (textArea: HTMLTextAreaElement) => { - debounce((element: HTMLTextAreaElement) => { - element.style.height = '0px'; // Reset height - const scrollHeight = element.scrollHeight; - element.style.height = Math.min(scrollHeight, maxHeight) + 'px'; - }, 150)(textArea); - }, + const debouncedAutosize = useMemo( + () => debounce((element: HTMLTextAreaElement) => { + element.style.height = '0px'; // Reset height + const scrollHeight = element.scrollHeight; + element.style.height = Math.min(scrollHeight, maxHeight) + 'px'; + }, 150), [maxHeight] ); diff --git a/ui/desktop/src/components/FlappyGoose.tsx b/ui/desktop/src/components/FlappyGoose.tsx index 3cd8b589bc23..5cd9cb35534a 100644 --- a/ui/desktop/src/components/FlappyGoose.tsx +++ b/ui/desktop/src/components/FlappyGoose.tsx @@ -216,7 +216,7 @@ const FlappyGoose: React.FC = ({ onClose }) => { useEffect(() => { const frames = [svg1, svg7]; frames.forEach((src, index) => { - const img = new Image(); + const img = new Image() as HTMLImageElement; img.src = src; img.onload = () => { framesLoaded.current += 1; diff --git a/ui/desktop/src/components/GoosehintsModal.tsx b/ui/desktop/src/components/GoosehintsModal.tsx index 835f6584109b..19624efb37d3 100644 --- a/ui/desktop/src/components/GoosehintsModal.tsx +++ b/ui/desktop/src/components/GoosehintsModal.tsx @@ -48,7 +48,7 @@ const ModalHelpText = () => ( ); -const ModalError = ({ error }: { error: any }) => ( +const ModalError = ({ error }: { error: Error }) => (
Error reading .goosehints file: {JSON.stringify(error)}
diff --git a/ui/desktop/src/components/RecipeEditor.tsx b/ui/desktop/src/components/RecipeEditor.tsx index fa4f2fa8c1b0..a621d32cb24d 100644 --- a/ui/desktop/src/components/RecipeEditor.tsx +++ b/ui/desktop/src/components/RecipeEditor.tsx @@ -125,7 +125,10 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { delete cleanExtension.enabled; // Remove legacy envs which could potentially include secrets // env_keys will work but rely on the end user having setup those keys themselves - delete cleanExtension.envs; + if ('envs' in cleanExtension) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + delete (cleanExtension as any).envs; + } return cleanExtension; }) .filter(Boolean) as FullExtensionConfig[], diff --git a/ui/desktop/src/components/conversation/SearchView.tsx b/ui/desktop/src/components/conversation/SearchView.tsx index 4f331dab37f1..5024a3328bf5 100644 --- a/ui/desktop/src/components/conversation/SearchView.tsx +++ b/ui/desktop/src/components/conversation/SearchView.tsx @@ -231,23 +231,19 @@ export const SearchView: React.FC> = ({ highlighterRef.current = null; } - // Cancel any pending highlight operations - debouncedHighlight.cancel?.(); - // Clear search when closing onSearch?.('', false); - }, [debouncedHighlight, onSearch]); + }, [onSearch]); - // Clean up highlighter and debounced functions on unmount + // Clean up highlighter on unmount useEffect(() => { return () => { if (highlighterRef.current) { highlighterRef.current.destroy(); highlighterRef.current = null; } - debouncedHighlight.cancel?.(); }; - }, [debouncedHighlight]); + }, []); // Listen for keyboard events useEffect(() => { diff --git a/ui/desktop/src/components/settings/OllamaBattleGame.tsx b/ui/desktop/src/components/settings/OllamaBattleGame.tsx index ae6a5f0d094b..b282640d8e01 100644 --- a/ui/desktop/src/components/settings/OllamaBattleGame.tsx +++ b/ui/desktop/src/components/settings/OllamaBattleGame.tsx @@ -24,6 +24,7 @@ interface OllamaBattleGameProps { export function OllamaBattleGame({ onComplete, requiredKeys: _ }: OllamaBattleGameProps) { // Use Audio element type for audioRef + // eslint-disable-next-line no-undef const audioRef = useRef(null); const [isMuted, setIsMuted] = useState(false); diff --git a/ui/desktop/src/components/settings/models/AddModelInline.tsx b/ui/desktop/src/components/settings/models/AddModelInline.tsx index b7d35d4e49e7..766d1725fe04 100644 --- a/ui/desktop/src/components/settings/models/AddModelInline.tsx +++ b/ui/desktop/src/components/settings/models/AddModelInline.tsx @@ -19,7 +19,7 @@ export function AddModelInline() { const [selectedProvider, setSelectedProvider] = useState(null); const [modelName, setModelName] = useState(''); - const [filteredModels, setFilteredModels] = useState([]); + const [filteredModels, setFilteredModels] = useState<{ id: string; name: string; provider: string }[]>([]); const [showSuggestions, setShowSuggestions] = useState(false); const handleModelSelection = useHandleModelSelection(); diff --git a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx index 4add680f5260..434e241d4f16 100644 --- a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx +++ b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx @@ -43,7 +43,7 @@ export function ConfigureProvidersGrid() { const [selectedForSetup, setSelectedForSetup] = useState(null); const [modalMode, setModalMode] = useState<'edit' | 'setup' | 'battle'>('setup'); const [isConfirmationOpen, setIsConfirmationOpen] = useState(false); - const [providerToDelete, setProviderToDelete] = useState(null); + const [providerToDelete, setProviderToDelete] = useState<{ name: string; id: string; isConfigured: boolean; description: string } | null>(null); const { currentModel } = useModel(); const providers = useMemo(() => { @@ -169,8 +169,8 @@ export function ConfigureProvidersGrid() { const confirmDelete = async () => { if (!providerToDelete) return; - - const requiredKeys = required_keys[providerToDelete.name]; + + const requiredKeys = required_keys[providerToDelete.name as keyof typeof required_keys]; if (!requiredKeys || requiredKeys.length === 0) { console.error(`No keys found for provider ${providerToDelete.name}`); return; diff --git a/ui/desktop/src/components/settings_v2/extensions/agent-api.ts b/ui/desktop/src/components/settings_v2/extensions/agent-api.ts index 243a91c2503c..d4b1e825aea1 100644 --- a/ui/desktop/src/components/settings_v2/extensions/agent-api.ts +++ b/ui/desktop/src/components/settings_v2/extensions/agent-api.ts @@ -29,11 +29,11 @@ export async function extensionApiCall( }; // for adding the payload is an extensionConfig, for removing payload is just the name - const extensionName = isActivating ? payload.name : payload; + const extensionName = isActivating ? (payload as ExtensionConfig).name : payload as string; let toastId; // Step 1: Show loading toast (only for activation of stdio) - if (isActivating && (payload as ExtensionConfig) && payload.type == 'stdio') { + if (isActivating && typeof payload === 'object' && payload.type === 'stdio') { toastId = toastService.loading({ title: extensionName, msg: `${action.verb} ${extensionName} extension...`, diff --git a/ui/desktop/src/components/settings_v2/extensions/utils.ts b/ui/desktop/src/components/settings_v2/extensions/utils.ts index 4561683680a5..235b897f0166 100644 --- a/ui/desktop/src/components/settings_v2/extensions/utils.ts +++ b/ui/desktop/src/components/settings_v2/extensions/utils.ts @@ -83,7 +83,7 @@ export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFo cmd: extension.type === 'stdio' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined, endpoint: extension.type === 'sse' ? extension.uri : undefined, enabled: extension.enabled, - timeout: extension.timeout, + timeout: 'timeout' in extension ? extension.timeout : undefined, envVars, }; } diff --git a/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx index 832c3db2cf70..99d6cd9326c1 100644 --- a/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx +++ b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx @@ -40,8 +40,8 @@ type AddModelModalProps = { export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => { const { getProviders, upsert } = useConfig(); const { switchModel } = useModel(); - const [providerOptions, setProviderOptions] = useState([]); - const [modelOptions, setModelOptions] = useState([]); + const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]); + const [modelOptions, setModelOptions] = useState<{ options: { value: string; label: string; provider: string }[] }[]>([]); const [provider, setProvider] = useState(null); const [model, setModel] = useState(''); const [isCustomModel, setIsCustomModel] = useState(false); @@ -169,7 +169,7 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => { }; // Store the original model options in state, initialized from modelOptions - const [originalModelOptions, setOriginalModelOptions] = useState(modelOptions); + const [originalModelOptions, setOriginalModelOptions] = useState<{ options: { value: string; label: string; provider: string }[] }[]>(modelOptions); const handleInputChange = (inputValue: string) => { if (!provider) return; diff --git a/ui/desktop/src/recipe/index.ts b/ui/desktop/src/recipe/index.ts index 17e51e308e57..aa185c611aa1 100644 --- a/ui/desktop/src/recipe/index.ts +++ b/ui/desktop/src/recipe/index.ts @@ -15,6 +15,8 @@ export interface Recipe { extensions?: FullExtensionConfig[]; goosehints?: string; context?: string[]; + profile?: string; + mcps?: number; } export interface CreateRecipeRequest {