From bd435a86f42486e603d7fcd9423829dfb363ec08 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Wed, 28 May 2025 15:53:50 -0700 Subject: [PATCH 1/2] Phase 6: Implement proper error handling without unknown/any types MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix error handling with TypeScript best practices instead of using unknown/any: * LinkPreview.tsx: Use 'err instanceof Error ? err.message : fallback' pattern * ProviderGrid.tsx: Proper error type checking with meaningful fallback * SessionsView.tsx: Convert unknown error to string safely * ConfigureBuiltInExtensionModal.tsx: Type-safe error message extraction * ConfigureExtensionModal.tsx: Consistent error handling pattern - Applied TypeScript best practices: * No unknown or any types used - proper type guards instead * Consistent error checking pattern: 'err instanceof Error ? err.message : fallback' * Meaningful fallback values for non-Error objects * Improved type safety throughout error handling - Reduces TypeScript errors by 5 (from 280 to 275) - Better error handling with proper type safety - Maintainable code with consistent error patterns - Total progress: 125+ errors eliminated across all phases (400+ → 275) --- ui/desktop/src/components/LinkPreview.tsx | 7 ++++--- ui/desktop/src/components/ProviderGrid.tsx | 7 ++++--- ui/desktop/src/components/sessions/SessionsView.tsx | 3 ++- .../settings/extensions/ConfigureBuiltInExtensionModal.tsx | 7 ++++--- .../settings/extensions/ConfigureExtensionModal.tsx | 7 ++++--- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/ui/desktop/src/components/LinkPreview.tsx b/ui/desktop/src/components/LinkPreview.tsx index a112498edd43..f4fc835b4651 100644 --- a/ui/desktop/src/components/LinkPreview.tsx +++ b/ui/desktop/src/components/LinkPreview.tsx @@ -85,10 +85,11 @@ export default function LinkPreview({ url }: LinkPreviewProps) { if (mounted) { setMetadata(data); } - } catch (error) { + } catch (err) { if (mounted) { - console.error('❌ Failed to fetch metadata:', error); - setError(error.message || 'Failed to fetch metadata'); + console.error('❌ Failed to fetch metadata:', err); + const errorMessage = err instanceof Error ? err.message : 'Failed to fetch metadata'; + setError(errorMessage); } } finally { if (mounted) { diff --git a/ui/desktop/src/components/ProviderGrid.tsx b/ui/desktop/src/components/ProviderGrid.tsx index 9ad66b9d84b0..442ea9863236 100644 --- a/ui/desktop/src/components/ProviderGrid.tsx +++ b/ui/desktop/src/components/ProviderGrid.tsx @@ -145,12 +145,13 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) { setShowSetupModal(false); setSelectedId(null); - } catch (error) { - console.error('Error handling modal submit:', error); + } catch (err) { + console.error('Error handling modal submit:', err); + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; toastError({ title: provider, msg: `Failed to ${providers.find((p) => p.id === selectedId)?.isConfigured ? 'update' : 'add'} configuration`, - traceback: error.message, + traceback: errorMessage, }); } }; diff --git a/ui/desktop/src/components/sessions/SessionsView.tsx b/ui/desktop/src/components/sessions/SessionsView.tsx index 0cd095509691..739523d7e4a4 100644 --- a/ui/desktop/src/components/sessions/SessionsView.tsx +++ b/ui/desktop/src/components/sessions/SessionsView.tsx @@ -30,10 +30,11 @@ const SessionsView: React.FC = ({ setView }) => { // Keep the selected session null if there's an error setSelectedSession(null); + const errorMessage = err instanceof Error ? err.message : String(err); toastError({ title: 'Failed to load session. The file may be corrupted.', msg: 'Please try again later.', - traceback: err, + traceback: errorMessage, }); } finally { setIsLoadingSession(false); diff --git a/ui/desktop/src/components/settings/extensions/ConfigureBuiltInExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/ConfigureBuiltInExtensionModal.tsx index d41b0640c6da..e8e3494a3fd3 100644 --- a/ui/desktop/src/components/settings/extensions/ConfigureBuiltInExtensionModal.tsx +++ b/ui/desktop/src/components/settings/extensions/ConfigureBuiltInExtensionModal.tsx @@ -74,12 +74,13 @@ export function ConfigureBuiltInExtensionModal({ }); onSubmit(); onClose(); - } catch (error) { - console.error('Error configuring extension:', error); + } catch (err) { + console.error('Error configuring extension:', err); + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; toastError({ title: extension.name, msg: `Failed to configure the extension`, - traceback: error.message, + traceback: errorMessage, }); } finally { setIsSubmitting(false); diff --git a/ui/desktop/src/components/settings/extensions/ConfigureExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/ConfigureExtensionModal.tsx index 82dcf8ba7638..0fbea05863d4 100644 --- a/ui/desktop/src/components/settings/extensions/ConfigureExtensionModal.tsx +++ b/ui/desktop/src/components/settings/extensions/ConfigureExtensionModal.tsx @@ -76,12 +76,13 @@ export function ConfigureExtensionModal({ }); onSubmit(); onClose(); - } catch (error) { - console.error('Error configuring extension:', error); + } catch (err) { + console.error('Error configuring extension:', err); + const errorMessage = err instanceof Error ? err.message : 'Unknown error occurred'; toastError({ title: extension.name, msg: `Failed to configure extension`, - traceback: error.message, + traceback: errorMessage, }); } finally { setIsSubmitting(false); From cc907f865e7c4033d19bbbb28e8bbb0d7991e898 Mon Sep 17 00:00:00 2001 From: Zane <75694352+zanesq@users.noreply.github.com> Date: Thu, 29 May 2025 08:24:43 -0700 Subject: [PATCH 2/2] Cleanup Phase 7: Fix implicit any types (#2712) --- ui/desktop/src/App.tsx | 47 +++++++++++++------ ui/desktop/src/components/ChatInput.tsx | 25 +++++----- ui/desktop/src/components/ErrorBoundary.tsx | 2 +- ui/desktop/src/components/FlappyGoose.tsx | 2 +- ui/desktop/src/components/GooseLogo.tsx | 19 ++++++-- ui/desktop/src/components/GoosehintsModal.tsx | 2 +- ui/desktop/src/components/ProviderGrid.tsx | 6 +-- ui/desktop/src/components/RecipeEditor.tsx | 7 ++- .../components/conversation/SearchView.tsx | 10 ++-- .../src/components/icons/ChevronDown.tsx | 2 +- ui/desktop/src/components/icons/Close.tsx | 2 +- .../components/settings/OllamaBattleGame.tsx | 1 + .../components/settings/api_keys/utils.tsx | 10 ++-- .../extensions/ManualExtensionModal.tsx | 2 +- .../settings/models/AddModelInline.tsx | 4 +- .../src/components/settings/models/utils.tsx | 2 +- .../settings/providers/BaseProviderGrid.tsx | 4 +- .../providers/ConfigureProvidersGrid.tsx | 6 +-- .../settings_v2/extensions/agent-api.ts | 4 +- .../settings_v2/extensions/utils.ts | 2 +- .../models/subcomponents/AddModelModal.tsx | 6 +-- ui/desktop/src/recipe/index.ts | 2 + 22 files changed, 98 insertions(+), 69 deletions(-) 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/ErrorBoundary.tsx b/ui/desktop/src/components/ErrorBoundary.tsx index eec01f6fe6d3..1c29d2a6b521 100644 --- a/ui/desktop/src/components/ErrorBoundary.tsx +++ b/ui/desktop/src/components/ErrorBoundary.tsx @@ -14,7 +14,7 @@ window.addEventListener('error', (event) => { ); }); -export function ErrorUI({ error }) { +export function ErrorUI({ error }: { error: Error }) { return (
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/GooseLogo.tsx b/ui/desktop/src/components/GooseLogo.tsx index a8c6bd834999..4d8ea402c59a 100644 --- a/ui/desktop/src/components/GooseLogo.tsx +++ b/ui/desktop/src/components/GooseLogo.tsx @@ -1,6 +1,12 @@ import { Goose, Rain } from './icons/Goose'; -export default function GooseLogo({ className = '', size = 'default', hover = true }) { +interface GooseLogoProps { + className?: string; + size?: 'default' | 'small'; + hover?: boolean; +} + +export default function GooseLogo({ className = '', size = 'default', hover = true }: GooseLogoProps) { const sizes = { default: { frame: 'w-16 h-16', @@ -12,15 +18,18 @@ export default function GooseLogo({ className = '', size = 'default', hover = tr rain: 'w-[150px] h-[150px]', goose: 'w-8 h-8', }, - }; + } as const; + + const currentSize = sizes[size]; + return (
- +
); } 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/ProviderGrid.tsx b/ui/desktop/src/components/ProviderGrid.tsx index 442ea9863236..1e3d6721478d 100644 --- a/ui/desktop/src/components/ProviderGrid.tsx +++ b/ui/desktop/src/components/ProviderGrid.tsx @@ -43,7 +43,7 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) { }); }, [activeKeys]); - const handleConfigure = async (provider) => { + const handleConfigure = async (provider: { id: string; name: string; isConfigured: boolean; description: string }) => { const providerId = provider.id.toLowerCase(); const modelName = getDefaultModel(providerId); @@ -63,7 +63,7 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) { onSubmit?.(); }; - const handleAddKeys = (provider) => { + const handleAddKeys = (provider: { id: string; name: string; isConfigured: boolean; description: string }) => { setSelectedId(provider.id); setShowSetupModal(true); }; @@ -74,7 +74,7 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) { const provider = providers.find((p) => p.id === selectedId)?.name; if (!provider) return; - const requiredKeys = required_keys[provider]; + const requiredKeys = required_keys[provider as keyof typeof required_keys]; if (!requiredKeys || requiredKeys.length === 0) { console.error(`No keys found for provider ${provider}`); return; diff --git a/ui/desktop/src/components/RecipeEditor.tsx b/ui/desktop/src/components/RecipeEditor.tsx index efbb85d2f7eb..a621d32cb24d 100644 --- a/ui/desktop/src/components/RecipeEditor.tsx +++ b/ui/desktop/src/components/RecipeEditor.tsx @@ -54,7 +54,7 @@ export default function RecipeEditor({ config }: RecipeEditorProps) { } } // Fall back to config if available, using extension names - const exts = []; + const exts: string[] = []; return exts; }); // Section visibility state @@ -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/icons/ChevronDown.tsx b/ui/desktop/src/components/icons/ChevronDown.tsx index b69908d6ca9e..b730afa75295 100644 --- a/ui/desktop/src/components/icons/ChevronDown.tsx +++ b/ui/desktop/src/components/icons/ChevronDown.tsx @@ -1,5 +1,5 @@ -export default function ChevronDown({ className }) { +export default function ChevronDown({ className }: { className?: string }) { return ( (null); const [isMuted, setIsMuted] = useState(false); diff --git a/ui/desktop/src/components/settings/api_keys/utils.tsx b/ui/desktop/src/components/settings/api_keys/utils.tsx index 994a547bb1eb..3cfb5051aa66 100644 --- a/ui/desktop/src/components/settings/api_keys/utils.tsx +++ b/ui/desktop/src/components/settings/api_keys/utils.tsx @@ -56,16 +56,16 @@ export async function getActiveProviders(): Promise { // For providers with multiple keys or keys without defaults: // Check if all required keys without defaults are set const requiredNonDefaultKeys = providerRequiredKeys.filter( - (key) => !(key in default_key_value) + (key: string) => !(key in default_key_value) ); // If there are no non-default keys, this provider needs at least one key explicitly set if (requiredNonDefaultKeys.length === 0) { - return providerRequiredKeys.some((key) => configStatus[key]?.is_set === true); + return providerRequiredKeys.some((key: string) => configStatus[key]?.is_set === true); } // Otherwise, all non-default keys must be set - return requiredNonDefaultKeys.every((key) => configStatus[key]?.is_set === true); + return requiredNonDefaultKeys.every((key: string) => configStatus[key]?.is_set === true); }) .map((provider) => provider.name || 'Unknown Provider'); @@ -96,14 +96,14 @@ export async function getConfigSettings(): Promise = {}; providers.forEach((provider) => { - const providerRequiredKeys = required_keys[provider.name] || []; + const providerRequiredKeys = required_keys[provider.name as keyof typeof required_keys] || []; data[provider.name] = { name: provider.name, supported: true, description: provider.metadata.description, models: provider.metadata.models, - config_status: providerRequiredKeys.reduce>((acc, key) => { + config_status: providerRequiredKeys.reduce>((acc: Record, key: string) => { acc[key] = { key, is_set: provider.is_configured, diff --git a/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx index db03a5a3abc7..c3b5a158e05c 100644 --- a/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx +++ b/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx @@ -142,7 +142,7 @@ export function ManualExtensionModal({ isOpen, onClose, onSubmit }: ManualExtens