Fix remaining typescript errors (#2741)

This commit is contained in:
Zane
2025-05-30 12:06:49 -07:00
committed by GitHub
parent 2c0cda7bec
commit 0c042010ff
109 changed files with 1873 additions and 1438 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -26,7 +26,7 @@
"test-e2e:report": "playwright show-report", "test-e2e:report": "playwright show-report",
"test-e2e:single": "npm run generate-api && playwright test -g", "test-e2e:single": "npm run generate-api && playwright test -g",
"lint": "eslint \"src/**/*.{ts,tsx}\" --fix --no-warn-ignored", "lint": "eslint \"src/**/*.{ts,tsx}\" --fix --no-warn-ignored",
"lint:check": "eslint \"src/**/*.{ts,tsx}\" --max-warnings 0 --no-warn-ignored", "lint:check": "npm run typecheck && eslint \"src/**/*.{ts,tsx}\" --max-warnings 0 --no-warn-ignored",
"format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"", "format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"", "format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"",
"prepare": "cd ../.. && husky install", "prepare": "cd ../.. && husky install",
@@ -67,12 +67,14 @@
"postcss": "^8.4.47", "postcss": "^8.4.47",
"prettier": "^3.4.2", "prettier": "^3.4.2",
"tailwindcss": "^3.4.14", "tailwindcss": "^3.4.14",
"typescript": "~5.5.0",
"vite": "^6.3.4" "vite": "^6.3.4"
}, },
"keywords": [], "keywords": [],
"license": "Apache-2.0", "license": "Apache-2.0",
"lint-staged": { "lint-staged": {
"src/**/*.{ts,tsx}": [ "src/**/*.{ts,tsx}": [
"bash -c 'npm run typecheck'",
"eslint --fix --max-warnings 0 --no-warn-ignored", "eslint --fix --max-warnings 0 --no-warn-ignored",
"prettier --write" "prettier --write"
], ],

View File

@@ -1,6 +1,7 @@
import { useEffect, useRef, useState } from 'react'; import { useEffect, useRef, useState } from 'react';
import { IpcRendererEvent } from 'electron'; import { IpcRendererEvent } from 'electron';
import { openSharedSessionFromDeepLink } from './sessionLinks'; import { openSharedSessionFromDeepLink, type SessionLinksViewOptions } from './sessionLinks';
import { type SharedSessionDetails } from './sharedSessions';
import { initializeSystem } from './utils/providerUtils'; import { initializeSystem } from './utils/providerUtils';
import { ErrorUI } from './components/ErrorBoundary'; import { ErrorUI } from './components/ErrorBoundary';
import { ConfirmationModal } from './components/ui/ConfirmationModal'; import { ConfirmationModal } from './components/ui/ConfirmationModal';
@@ -9,6 +10,7 @@ import { toastService } from './toasts';
import { extractExtensionName } from './components/settings/extensions/utils'; import { extractExtensionName } from './components/settings/extensions/utils';
import { GoosehintsModal } from './components/GoosehintsModal'; import { GoosehintsModal } from './components/GoosehintsModal';
import { type ExtensionConfig } from './extensions'; import { type ExtensionConfig } from './extensions';
import { type Recipe } from './recipe';
import ChatView from './components/ChatView'; import ChatView from './components/ChatView';
import SuspenseLoader from './suspense-loader'; import SuspenseLoader from './suspense-loader';
@@ -52,20 +54,20 @@ export type ViewOptions = {
extensionId?: string; extensionId?: string;
showEnvVars?: boolean; showEnvVars?: boolean;
deepLinkConfig?: ExtensionConfig; deepLinkConfig?: ExtensionConfig;
// Session view options // Session view options
resumedSession?: SessionDetails; resumedSession?: SessionDetails;
sessionDetails?: SessionDetails; sessionDetails?: SessionDetails;
error?: string; error?: string;
shareToken?: string; shareToken?: string;
baseUrl?: string; baseUrl?: string;
// Recipe editor options // Recipe editor options
config?: unknown; config?: unknown;
// Permission view options // Permission view options
parentView?: View; parentView?: View;
// Generic options // Generic options
[key: string]: unknown; [key: string]: unknown;
}; };
@@ -237,12 +239,18 @@ export default function App() {
}, []); }, []);
useEffect(() => { useEffect(() => {
const handleOpenSharedSession = async (_event: IpcRendererEvent, link: string) => { const handleOpenSharedSession = async (_event: IpcRendererEvent, ...args: unknown[]) => {
const link = args[0] as string;
window.electron.logInfo(`Opening shared session from deep link ${link}`); window.electron.logInfo(`Opening shared session from deep link ${link}`);
setIsLoadingSharedSession(true); setIsLoadingSharedSession(true);
setSharedSessionError(null); setSharedSessionError(null);
try { try {
await openSharedSessionFromDeepLink(link, setView); await openSharedSessionFromDeepLink(
link,
(view: View, options?: SessionLinksViewOptions) => {
setView(view, options as ViewOptions);
}
);
} catch (error) { } catch (error) {
console.error('Unexpected error opening shared session:', error); console.error('Unexpected error opening shared session:', error);
setView('sessions'); setView('sessions');
@@ -279,7 +287,8 @@ export default function App() {
useEffect(() => { useEffect(() => {
console.log('Setting up fatal error handler'); console.log('Setting up fatal error handler');
const handleFatalError = (_event: IpcRendererEvent, errorMessage: string) => { const handleFatalError = (_event: IpcRendererEvent, ...args: unknown[]) => {
const errorMessage = args[0] as string;
console.error('Encountered a fatal error: ', errorMessage); console.error('Encountered a fatal error: ', errorMessage);
console.error('Current view:', view); console.error('Current view:', view);
console.error('Is loading session:', isLoadingSession); console.error('Is loading session:', isLoadingSession);
@@ -293,7 +302,8 @@ export default function App() {
useEffect(() => { useEffect(() => {
console.log('Setting up view change handler'); console.log('Setting up view change handler');
const handleSetView = (_event: IpcRendererEvent, newView: View) => { const handleSetView = (_event: IpcRendererEvent, ...args: unknown[]) => {
const newView = args[0] as View;
console.log(`Received view change request to: ${newView}`); console.log(`Received view change request to: ${newView}`);
setView(newView); setView(newView);
}; };
@@ -328,7 +338,8 @@ export default function App() {
useEffect(() => { useEffect(() => {
console.log('Setting up extension handler'); console.log('Setting up extension handler');
const handleAddExtension = async (_event: IpcRendererEvent, link: string) => { const handleAddExtension = async (_event: IpcRendererEvent, ...args: unknown[]) => {
const link = args[0] as string;
try { try {
console.log(`Received add-extension event with link: ${link}`); console.log(`Received add-extension event with link: ${link}`);
const command = extractCommand(link); const command = extractCommand(link);
@@ -401,7 +412,7 @@ export default function App() {
}, [STRICT_ALLOWLIST]); }, [STRICT_ALLOWLIST]);
useEffect(() => { useEffect(() => {
const handleFocusInput = (_event: IpcRendererEvent) => { const handleFocusInput = (_event: IpcRendererEvent, ..._args: unknown[]) => {
const inputField = document.querySelector('input[type="text"], textarea') as HTMLInputElement; const inputField = document.querySelector('input[type="text"], textarea') as HTMLInputElement;
if (inputField) { if (inputField) {
inputField.focus(); inputField.focus();
@@ -418,7 +429,9 @@ export default function App() {
console.log(`Confirming installation of extension from: ${pendingLink}`); console.log(`Confirming installation of extension from: ${pendingLink}`);
setModalVisible(false); setModalVisible(false);
try { try {
await addExtensionFromDeepLinkV2(pendingLink, addExtension, setView); await addExtensionFromDeepLinkV2(pendingLink, addExtension, (view: string, options) => {
setView(view as View, options as ViewOptions);
});
console.log('Extension installation successful'); console.log('Extension installation successful');
} catch (error) { } catch (error) {
console.error('Failed to add extension:', error); console.error('Failed to add extension:', error);
@@ -522,7 +535,9 @@ export default function App() {
{view === 'schedules' && <SchedulesView onClose={() => setView('chat')} />} {view === 'schedules' && <SchedulesView onClose={() => setView('chat')} />}
{view === 'sharedSession' && ( {view === 'sharedSession' && (
<SharedSessionView <SharedSessionView
session={viewOptions?.sessionDetails} session={
(viewOptions?.sessionDetails as unknown as SharedSessionDetails | null) || null
}
isLoading={isLoadingSharedSession} isLoading={isLoadingSharedSession}
error={viewOptions?.error || sharedSessionError} error={viewOptions?.error || sharedSessionError}
onBack={() => setView('sessions')} onBack={() => setView('sessions')}
@@ -532,7 +547,9 @@ export default function App() {
try { try {
await openSharedSessionFromDeepLink( await openSharedSessionFromDeepLink(
`goose://sessions/${viewOptions.shareToken}`, `goose://sessions/${viewOptions.shareToken}`,
setView, (view: View, options?: SessionLinksViewOptions) => {
setView(view, options as ViewOptions);
},
viewOptions.baseUrl viewOptions.baseUrl
); );
} catch (error) { } catch (error) {
@@ -546,7 +563,7 @@ export default function App() {
)} )}
{view === 'recipeEditor' && ( {view === 'recipeEditor' && (
<RecipeEditor <RecipeEditor
config={viewOptions?.config || window.electron.getConfig().recipeConfig} config={(viewOptions?.config as Recipe) || window.electron.getConfig().recipeConfig}
/> />
)} )}
{view === 'permission' && ( {view === 'permission' && (

View File

@@ -1,4 +1,3 @@
interface AgentHeaderProps { interface AgentHeaderProps {
title: string; title: string;
profileInfo?: string; profileInfo?: string;

View File

@@ -94,11 +94,7 @@ export default function ChatInput({
// Set the image to loading state // Set the image to loading state
setPastedImages((prev) => setPastedImages((prev) =>
prev.map((img) => prev.map((img) => (img.id === imageId ? { ...img, isLoading: true, error: undefined } : img))
img.id === imageId
? { ...img, isLoading: true, error: undefined }
: img
)
); );
try { try {
@@ -149,19 +145,21 @@ export default function ChatInput({
// Debounced function to update actual value // Debounced function to update actual value
const debouncedSetValue = useMemo( const debouncedSetValue = useMemo(
() => debounce((value: string) => { () =>
setValue(value); debounce((value: string) => {
}, 150), setValue(value);
}, 150),
[setValue] [setValue]
); );
// Debounced autosize function // Debounced autosize function
const debouncedAutosize = useMemo( const debouncedAutosize = useMemo(
() => debounce((element: HTMLTextAreaElement) => { () =>
element.style.height = '0px'; // Reset height debounce((element: HTMLTextAreaElement) => {
const scrollHeight = element.scrollHeight; element.style.height = '0px'; // Reset height
element.style.height = Math.min(scrollHeight, maxHeight) + 'px'; const scrollHeight = element.scrollHeight;
}, 150), element.style.height = Math.min(scrollHeight, maxHeight) + 'px';
}, 150),
[maxHeight] [maxHeight]
); );
@@ -179,10 +177,10 @@ export default function ChatInput({
const handlePaste = async (evt: React.ClipboardEvent<HTMLTextAreaElement>) => { const handlePaste = async (evt: React.ClipboardEvent<HTMLTextAreaElement>) => {
const files = Array.from(evt.clipboardData.files || []); const files = Array.from(evt.clipboardData.files || []);
const imageFiles = files.filter(file => file.type.startsWith('image/')); const imageFiles = files.filter((file) => file.type.startsWith('image/'));
if (imageFiles.length === 0) return; if (imageFiles.length === 0) return;
// Check if adding these images would exceed the limit // Check if adding these images would exceed the limit
if (pastedImages.length + imageFiles.length > MAX_IMAGES_PER_MESSAGE) { if (pastedImages.length + imageFiles.length > MAX_IMAGES_PER_MESSAGE) {
// Show error message to user // Show error message to user
@@ -192,20 +190,20 @@ export default function ChatInput({
id: `error-${Date.now()}`, id: `error-${Date.now()}`,
dataUrl: '', dataUrl: '',
isLoading: false, isLoading: false,
error: `Cannot paste ${imageFiles.length} image(s). Maximum ${MAX_IMAGES_PER_MESSAGE} images per message allowed.` error: `Cannot paste ${imageFiles.length} image(s). Maximum ${MAX_IMAGES_PER_MESSAGE} images per message allowed.`,
} },
]); ]);
// Remove the error message after 3 seconds // Remove the error message after 3 seconds
setTimeout(() => { setTimeout(() => {
setPastedImages((prev) => prev.filter(img => !img.id.startsWith('error-'))); setPastedImages((prev) => prev.filter((img) => !img.id.startsWith('error-')));
}, 3000); }, 3000);
return; return;
} }
evt.preventDefault(); evt.preventDefault();
for (const file of imageFiles) { for (const file of imageFiles) {
// Check individual file size before processing // Check individual file size before processing
if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) { if (file.size > MAX_IMAGE_SIZE_MB * 1024 * 1024) {
@@ -216,18 +214,18 @@ export default function ChatInput({
id: errorId, id: errorId,
dataUrl: '', dataUrl: '',
isLoading: false, isLoading: false,
error: `Image too large (${Math.round(file.size / (1024 * 1024))}MB). Maximum ${MAX_IMAGE_SIZE_MB}MB allowed.` error: `Image too large (${Math.round(file.size / (1024 * 1024))}MB). Maximum ${MAX_IMAGE_SIZE_MB}MB allowed.`,
} },
]); ]);
// Remove the error message after 3 seconds // Remove the error message after 3 seconds
setTimeout(() => { setTimeout(() => {
setPastedImages((prev) => prev.filter(img => img.id !== errorId)); setPastedImages((prev) => prev.filter((img) => img.id !== errorId));
}, 3000); }, 3000);
continue; continue;
} }
const reader = new FileReader(); const reader = new FileReader();
reader.onload = async (e) => { reader.onload = async (e) => {
const dataUrl = e.target?.result as string; const dataUrl = e.target?.result as string;
@@ -365,7 +363,9 @@ export default function ChatInput({
LocalMessageStorage.addMessage(validPastedImageFilesPaths.join(' ')); LocalMessageStorage.addMessage(validPastedImageFilesPaths.join(' '));
} }
handleSubmit(new CustomEvent('submit', { detail: { value: textToSend } })); handleSubmit(
new CustomEvent('submit', { detail: { value: textToSend } }) as unknown as React.FormEvent
);
setDisplayValue(''); setDisplayValue('');
setValue(''); setValue('');
@@ -502,7 +502,7 @@ export default function ChatInput({
className="absolute -top-1 -right-1 bg-gray-700 hover:bg-red-600 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs leading-none opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity z-10" className="absolute -top-1 -right-1 bg-gray-700 hover:bg-red-600 text-white rounded-full w-5 h-5 flex items-center justify-center text-xs leading-none opacity-0 group-hover:opacity-100 focus:opacity-100 transition-opacity z-10"
aria-label="Remove image" aria-label="Remove image"
> >
<Close size={14} /> <Close className="w-3.5 h-3.5" />
</button> </button>
)} )}
</div> </div>

View File

@@ -34,6 +34,7 @@ import {
ToolResponseMessageContent, ToolResponseMessageContent,
ToolConfirmationRequestMessageContent, ToolConfirmationRequestMessageContent,
getTextContent, getTextContent,
TextContent,
} from '../types/message'; } from '../types/message';
export interface ChatType { export interface ChatType {
@@ -245,12 +246,20 @@ function ChatContent({
// Create a new window for the recipe editor // Create a new window for the recipe editor
console.log('Opening recipe editor with config:', response.recipe); console.log('Opening recipe editor with config:', response.recipe);
const recipeConfig = {
id: response.recipe.title || 'untitled',
name: response.recipe.title || 'Untitled Recipe',
description: response.recipe.description || '',
instructions: response.recipe.instructions || '',
activities: response.recipe.activities || [],
prompt: response.recipe.prompt || '',
};
window.electron.createChatWindow( window.electron.createChatWindow(
undefined, // query undefined, // query
undefined, // dir undefined, // dir
undefined, // version undefined, // version
undefined, // resumeSessionId undefined, // resumeSessionId
response.recipe, // recipe config recipeConfig, // recipe config
'recipeEditor' // view type 'recipeEditor' // view type
); );
@@ -273,11 +282,8 @@ function ChatContent({
// Update chat messages when they change and save to sessionStorage // Update chat messages when they change and save to sessionStorage
useEffect(() => { useEffect(() => {
setChat((prevChat: ChatType) => { setChat({ ...chat, messages });
const updatedChat = { ...prevChat, messages }; }, [messages, setChat, chat]);
return updatedChat;
});
}, [messages, setChat]);
useEffect(() => { useEffect(() => {
if (messages.length > 0) { if (messages.length > 0) {
@@ -354,10 +360,11 @@ function ChatContent({
// check if the last message is a real user's message // check if the last message is a real user's message
if (lastMessage && isUserMessage(lastMessage) && !isToolResponse) { if (lastMessage && isUserMessage(lastMessage) && !isToolResponse) {
// Get the text content from the last message before removing it // Get the text content from the last message before removing it
const textContent = lastMessage.content.find((c) => c.type === 'text')?.text || ''; const textContent = lastMessage.content.find((c): c is TextContent => c.type === 'text');
const textValue = textContent?.text || '';
// Set the text back to the input field // Set the text back to the input field
_setInput(textContent); _setInput(textValue);
// Remove the last user message if it's the most recent one // Remove the last user message if it's the most recent one
if (messages.length > 1) { if (messages.length > 1) {
@@ -453,7 +460,8 @@ function ChatContent({
return filteredMessages return filteredMessages
.reduce<string[]>((history, message) => { .reduce<string[]>((history, message) => {
if (isUserMessage(message)) { if (isUserMessage(message)) {
const text = message.content.find((c) => c.type === 'text')?.text?.trim(); const textContent = message.content.find((c): c is TextContent => c.type === 'text');
const text = textContent?.text?.trim();
if (text) { if (text) {
history.push(text); history.push(text);
} }
@@ -468,7 +476,7 @@ function ChatContent({
const fetchSessionTokens = async () => { const fetchSessionTokens = async () => {
try { try {
const sessionDetails = await fetchSessionDetails(chat.id); const sessionDetails = await fetchSessionDetails(chat.id);
setSessionTokenCount(sessionDetails.metadata.total_tokens); setSessionTokenCount(sessionDetails.metadata.total_tokens || 0);
} catch (err) { } catch (err) {
console.error('Error fetching session token count:', err); console.error('Error fetching session token count:', err);
} }
@@ -535,7 +543,7 @@ function ChatContent({
{messages.length === 0 ? ( {messages.length === 0 ? (
<Splash <Splash
append={append} append={append}
activities={Array.isArray(recipeConfig?.activities) ? recipeConfig.activities : null} activities={Array.isArray(recipeConfig?.activities) ? recipeConfig!.activities : null}
title={recipeConfig?.title} title={recipeConfig?.title}
/> />
) : ( ) : (

View File

@@ -148,7 +148,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
return extensionsList; return extensionsList;
} }
const extensionResponse: ExtensionResponse = result.data; const extensionResponse: ExtensionResponse = result.data!;
setExtensionsList(extensionResponse.extensions); setExtensionsList(extensionResponse.extensions);
return extensionResponse.extensions; return extensionResponse.extensions;
} }
@@ -173,8 +173,8 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
async (forceRefresh = false): Promise<ProviderDetails[]> => { async (forceRefresh = false): Promise<ProviderDetails[]> => {
if (forceRefresh || providersList.length === 0) { if (forceRefresh || providersList.length === 0) {
const response = await providers(); const response = await providers();
setProvidersList(response.data); setProvidersList(response.data || []);
return response.data; return response.data || [];
} }
return providersList; return providersList;
}, },
@@ -191,7 +191,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
// Load providers // Load providers
try { try {
const providersResponse = await providers(); const providersResponse = await providers();
setProvidersList(providersResponse.data); setProvidersList(providersResponse.data || []);
} catch (error) { } catch (error) {
console.error('Failed to load providers:', error); console.error('Failed to load providers:', error);
} }

View File

@@ -51,7 +51,7 @@ export function ErrorUI({ error }: { error: Error }) {
export class ErrorBoundary extends React.Component< export class ErrorBoundary extends React.Component<
{ children: React.ReactNode }, { children: React.ReactNode },
{ error: Error; hasError: boolean } { error: Error | null; hasError: boolean }
> { > {
constructor(props: { children: React.ReactNode }) { constructor(props: { children: React.ReactNode }) {
super(props); super(props);
@@ -69,7 +69,7 @@ export class ErrorBoundary extends React.Component<
render() { render() {
if (this.state.hasError) { if (this.state.hasError) {
return <ErrorUI error={this.state.error} />; return <ErrorUI error={this.state.error || new Error('Unknown error')} />;
} }
return this.props.children; return this.props.children;
} }

View File

@@ -1,11 +1,5 @@
import React, { useEffect, useRef, useState, useCallback } from 'react'; import React, { useEffect, useRef, useState, useCallback } from 'react';
declare var requestAnimationFrame: (callback: FrameRequestCallback) => number;
declare class HTMLCanvasElement {}
declare class HTMLImageElement {}
declare class DOMHighResTimeStamp {}
declare class Image {}
declare type FrameRequestCallback = (time: DOMHighResTimeStamp) => void;
import svg1 from '../images/loading-goose/1.svg'; import svg1 from '../images/loading-goose/1.svg';
import svg7 from '../images/loading-goose/7.svg'; import svg7 from '../images/loading-goose/7.svg';
@@ -20,9 +14,11 @@ interface FlappyGooseProps {
} }
const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => { const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => {
const canvasRef = useRef<HTMLCanvasElement>(null); // eslint-disable-next-line no-undef
const canvasRef = useRef<HTMLCanvasElement | null>(null);
const [gameOver, setGameOver] = useState(false); const [gameOver, setGameOver] = useState(false);
const [displayScore, setDisplayScore] = useState(0); const [displayScore, setDisplayScore] = useState(0);
// eslint-disable-next-line no-undef
const gooseImages = useRef<HTMLImageElement[]>([]); const gooseImages = useRef<HTMLImageElement[]>([]);
const framesLoaded = useRef(0); const framesLoaded = useRef(0);
const [imagesReady, setImagesReady] = useState(false); const [imagesReady, setImagesReady] = useState(false);
@@ -51,7 +47,7 @@ const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => {
const OBSTACLE_WIDTH = 40; const OBSTACLE_WIDTH = 40;
const FLAP_DURATION = 150; const FLAP_DURATION = 150;
const safeRequestAnimationFrame = useCallback((callback: FrameRequestCallback) => { const safeRequestAnimationFrame = useCallback((callback: (time: number) => void) => {
if (typeof window !== 'undefined' && typeof requestAnimationFrame !== 'undefined') { if (typeof window !== 'undefined' && typeof requestAnimationFrame !== 'undefined') {
requestAnimationFrame(callback); requestAnimationFrame(callback);
} }
@@ -216,6 +212,7 @@ const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => {
useEffect(() => { useEffect(() => {
const frames = [svg1, svg7]; const frames = [svg1, svg7];
frames.forEach((src, index) => { frames.forEach((src, index) => {
// eslint-disable-next-line no-undef
const img = new Image() as HTMLImageElement; const img = new Image() as HTMLImageElement;
img.src = src; img.src = src;
img.onload = () => { img.onload = () => {
@@ -272,7 +269,9 @@ const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => {
onClick={flap} onClick={flap}
> >
<canvas <canvas
ref={canvasRef} ref={(el) => {
canvasRef.current = el;
}}
style={{ style={{
border: '2px solid #333', border: '2px solid #333',
borderRadius: '8px', borderRadius: '8px',

View File

@@ -6,7 +6,11 @@ interface GooseLogoProps {
hover?: boolean; hover?: boolean;
} }
export default function GooseLogo({ className = '', size = 'default', hover = true }: GooseLogoProps) { export default function GooseLogo({
className = '',
size = 'default',
hover = true,
}: GooseLogoProps) {
const sizes = { const sizes = {
default: { default: {
frame: 'w-16 h-16', frame: 'w-16 h-16',
@@ -19,9 +23,9 @@ export default function GooseLogo({ className = '', size = 'default', hover = tr
goose: 'w-8 h-8', goose: 'w-8 h-8',
}, },
} as const; } as const;
const currentSize = sizes[size]; const currentSize = sizes[size];
return ( return (
<div <div
className={`${className} ${currentSize.frame} ${hover ? 'group/with-hover' : ''} relative overflow-hidden`} className={`${className} ${currentSize.frame} ${hover ? 'group/with-hover' : ''} relative overflow-hidden`}

View File

@@ -194,7 +194,7 @@ export default function GooseMessage({
{/* NOTE from alexhancock on 1/14/2025 - disabling again temporarily due to non-determinism in when the forms show up */} {/* NOTE from alexhancock on 1/14/2025 - disabling again temporarily due to non-determinism in when the forms show up */}
{false && metadata && ( {false && metadata && (
<div className="flex mt-[16px]"> <div className="flex mt-[16px]">
<GooseResponseForm message={textContent} metadata={metadata} append={append} /> <GooseResponseForm message={textContent} metadata={metadata || null} append={append} />
</div> </div>
)} )}
</div> </div>

View File

@@ -132,9 +132,14 @@ export default function GooseResponseForm({
return null; return null;
} }
function isForm(f: DynamicForm) { function isForm(f: DynamicForm | null): f is DynamicForm {
return ( return (
f && f.title && f.description && f.fields && Array.isArray(f.fields) && f.fields.length > 0 !!f &&
!!f.title &&
!!f.description &&
!!f.fields &&
Array.isArray(f.fields) &&
f.fields.length > 0
); );
} }

View File

@@ -96,9 +96,9 @@ type GoosehintsModalProps = {
export const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: GoosehintsModalProps) => { export const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: GoosehintsModalProps) => {
const goosehintsFilePath = `${directory}/.goosehints`; const goosehintsFilePath = `${directory}/.goosehints`;
const [goosehintsFile, setGoosehintsFile] = useState<string>(null); const [goosehintsFile, setGoosehintsFile] = useState<string>('');
const [goosehintsFileFound, setGoosehintsFileFound] = useState<boolean>(false); const [goosehintsFileFound, setGoosehintsFileFound] = useState<boolean>(false);
const [goosehintsFileReadError, setGoosehintsFileReadError] = useState<string>(null); const [goosehintsFileReadError, setGoosehintsFileReadError] = useState<string>('');
useEffect(() => { useEffect(() => {
const fetchGoosehintsFile = async () => { const fetchGoosehintsFile = async () => {
@@ -106,7 +106,7 @@ export const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: Goosehi
const { file, error, found } = await getGoosehintsFile(goosehintsFilePath); const { file, error, found } = await getGoosehintsFile(goosehintsFilePath);
setGoosehintsFile(file); setGoosehintsFile(file);
setGoosehintsFileFound(found); setGoosehintsFileFound(found);
setGoosehintsFileReadError(error); setGoosehintsFileReadError(error || '');
} catch (error) { } catch (error) {
console.error('Error fetching .goosehints file:', error); console.error('Error fetching .goosehints file:', error);
} }
@@ -125,7 +125,7 @@ export const GoosehintsModal = ({ directory, setIsGoosehintsModalOpen }: Goosehi
<ModalHelpText /> <ModalHelpText />
<div className="flex flex-col flex-1"> <div className="flex flex-col flex-1">
{goosehintsFileReadError ? ( {goosehintsFileReadError ? (
<ModalError error={goosehintsFileReadError} /> <ModalError error={new Error(goosehintsFileReadError)} />
) : ( ) : (
<div className="flex flex-col flex-1 space-y-2 h-full"> <div className="flex flex-col flex-1 space-y-2 h-full">
<ModalFileInfo filePath={goosehintsFilePath} found={goosehintsFileFound} /> <ModalFileInfo filePath={goosehintsFilePath} found={goosehintsFileFound} />

View File

@@ -54,9 +54,9 @@ async function fetchMetadata(url: string): Promise<Metadata> {
return { return {
title: title || url, title: title || url,
description, description: description || undefined,
favicon, favicon,
image, image: image || undefined,
url, url,
}; };
} catch (error) { } catch (error) {

View File

@@ -1,4 +1,3 @@
export function LoadingPlaceholder() { export function LoadingPlaceholder() {
return ( return (
<div className="space-y-2"> <div className="space-y-2">

View File

@@ -43,10 +43,15 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) {
}); });
}, [activeKeys]); }, [activeKeys]);
const handleConfigure = async (provider: { id: string; name: string; isConfigured: boolean; description: string }) => { const handleConfigure = async (provider: {
id: string;
name: string;
isConfigured: boolean;
description: string;
}) => {
const providerId = provider.id.toLowerCase(); const providerId = provider.id.toLowerCase();
const modelName = getDefaultModel(providerId); const modelName = getDefaultModel(providerId) || 'default-model';
const model = createSelectedModel(providerId, modelName); const model = createSelectedModel(providerId, modelName);
await initializeSystem(providerId, model.name); await initializeSystem(providerId, model.name);
@@ -63,7 +68,12 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) {
onSubmit?.(); onSubmit?.();
}; };
const handleAddKeys = (provider: { id: string; name: string; isConfigured: boolean; description: string }) => { const handleAddKeys = (provider: {
id: string;
name: string;
isConfigured: boolean;
description: string;
}) => {
setSelectedId(provider.id); setSelectedId(provider.id);
setShowSetupModal(true); setShowSetupModal(true);
}; };
@@ -189,9 +199,9 @@ export function ProviderGrid({ onSubmit }: ProviderGridProps) {
{showSetupModal && selectedId && ( {showSetupModal && selectedId && (
<div className="relative z-[9999]"> <div className="relative z-[9999]">
<ProviderSetupModal <ProviderSetupModal
provider={providers.find((p) => p.id === selectedId)?.name} provider={providers.find((p) => p.id === selectedId)?.name || 'Unknown Provider'}
model="Example Model" _model="Example Model"
endpoint="Example Endpoint" _endpoint="Example Endpoint"
onSubmit={handleModalSubmit} onSubmit={handleModalSubmit}
onCancel={() => { onCancel={() => {
setShowSetupModal(false); setShowSetupModal(false);

View File

@@ -123,13 +123,13 @@ export default function RecipeEditor({ config }: RecipeEditorProps) {
if (!extension) return null; if (!extension) return null;
// Create a clean copy of the extension configuration // Create a clean copy of the extension configuration
const cleanExtension = { ...extension }; const { enabled: _enabled, ...cleanExtension } = extension;
delete cleanExtension.enabled;
// Remove legacy envs which could potentially include secrets // Remove legacy envs which could potentially include secrets
// env_keys will work but rely on the end user having setup those keys themselves // env_keys will work but rely on the end user having setup those keys themselves
if ('envs' in cleanExtension) { if ('envs' in cleanExtension) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
delete (cleanExtension as any).envs; const { envs: _envs, ...finalExtension } = cleanExtension as any;
return finalExtension;
} }
return cleanExtension; return cleanExtension;
}) })

View File

@@ -129,7 +129,7 @@ function ToolCallView({
const toolResults: { result: Content; isExpandToolResults: boolean }[] = const toolResults: { result: Content; isExpandToolResults: boolean }[] =
loadingStatus === 'success' && Array.isArray(toolResponse?.toolResult.value) loadingStatus === 'success' && Array.isArray(toolResponse?.toolResult.value)
? toolResponse.toolResult.value ? toolResponse!.toolResult.value
.filter((item) => { .filter((item) => {
const audience = item.annotations?.audience as string[] | undefined; const audience = item.annotations?.audience as string[] | undefined;
return !audience || audience.includes('user'); return !audience || audience.includes('user');
@@ -322,7 +322,7 @@ function ToolLogsView({
working: boolean; working: boolean;
isStartExpanded?: boolean; isStartExpanded?: boolean;
}) { }) {
const boxRef = useRef(null); const boxRef = useRef<HTMLDivElement>(null);
// Whenever logs update, jump to the newest entry // Whenever logs update, jump to the newest entry
useEffect(() => { useEffect(() => {

View File

@@ -69,13 +69,13 @@ export const SearchBar: React.FC<SearchBarProps> = ({
} }
}, [initialSearchTerm, caseSensitive, debouncedSearchRef]); }, [initialSearchTerm, caseSensitive, debouncedSearchRef]);
const [localSearchResults, setLocalSearchResults] = useState<typeof searchResults>(null); const [localSearchResults, setLocalSearchResults] = useState<typeof searchResults>(undefined);
// Sync external search results with local state // Sync external search results with local state
useEffect(() => { useEffect(() => {
// Only set results if we have a search term // Only set results if we have a search term
if (!searchTerm) { if (!searchTerm) {
setLocalSearchResults(null); setLocalSearchResults(undefined);
} else { } else {
setLocalSearchResults(searchResults); setLocalSearchResults(searchResults);
} }

View File

@@ -314,7 +314,7 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
<div <div
ref={(el) => { ref={(el) => {
if (el) { if (el) {
containerRef.current = el; containerRef.current = el as SearchContainerElement;
// Expose the highlighter instance // Expose the highlighter instance
containerRef.current._searchHighlighter = highlighterRef.current; containerRef.current._searchHighlighter = highlighterRef.current;
} }
@@ -326,7 +326,7 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
onSearch={handleSearch} onSearch={handleSearch}
onClose={handleCloseSearch} onClose={handleCloseSearch}
onNavigate={handleNavigate} onNavigate={handleNavigate}
searchResults={searchResults || internalSearchResults} searchResults={searchResults || internalSearchResults || undefined}
inputRef={searchInputRef} inputRef={searchInputRef}
initialSearchTerm={initialSearchTerm} initialSearchTerm={initialSearchTerm}
/> />

View File

@@ -1,4 +1,3 @@
export default function ArrowDown({ className = '' }) { export default function ArrowDown({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function ArrowUp({ className = '' }) { export default function ArrowUp({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Attach({ className = '' }) { export default function Attach({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Back({ className = '' }) { export default function Back({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export function Bars() { export function Bars() {
return ( return (
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg"> <svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">

View File

@@ -1,4 +1,3 @@
export default function ChatSmart({ className = '' }) { export default function ChatSmart({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Check({ className = '' }) { export default function Check({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function ChevronDown({ className }: { className?: string }) { export default function ChevronDown({ className }: { className?: string }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
interface Props { interface Props {
className?: string; className?: string;
// eslint-disable-next-line // eslint-disable-next-line

View File

@@ -1,4 +1,3 @@
export default function ChevronUp({ className = '' }) { export default function ChevronUp({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Close({ className }: { className?: string }) { export default function Close({ className }: { className?: string }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Copy({ className = '' }) { export default function Copy({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Document({ className = '' }) { export default function Document({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Edit({ className = '' }) { export default function Edit({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export function Gear({ className = '' }: { className?: string }) { export function Gear({ className = '' }: { className?: string }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
interface Props { interface Props {
// eslint-disable-next-line // eslint-disable-next-line
[key: string]: any; // This will allow any other SVG props to pass through [key: string]: any; // This will allow any other SVG props to pass through

View File

@@ -1,4 +1,3 @@
export function Goose({ className = '' }) { export function Goose({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Idea({ className = '' }) { export default function Idea({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function More({ className = '' }) { export default function More({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Refresh({ className = '' }) { export default function Refresh({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Send({ className = '' }) { export default function Send({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function SensitiveHidden({ className = '' }) { export default function SensitiveHidden({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function SensitiveVisible({ className = '' }) { export default function SensitiveVisible({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Settings({ className = '' }) { export default function Settings({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function Time({ className = '' }) { export default function Time({ className = '' }) {
return ( return (
<svg <svg

View File

@@ -5,6 +5,15 @@ import { FolderOpen, Moon, Sliders, Sun } from 'lucide-react';
import { useConfig } from '../ConfigContext'; import { useConfig } from '../ConfigContext';
import { ViewOptions, View } from '../../App'; import { ViewOptions, View } from '../../App';
interface RecipeConfig {
id: string;
name: string;
description: string;
instructions?: string;
activities?: string[];
[key: string]: unknown;
}
interface MenuButtonProps { interface MenuButtonProps {
onClick: () => void; onClick: () => void;
children: React.ReactNode; children: React.ReactNode;
@@ -187,7 +196,7 @@ export default function MoreMenu({
setOpen(false); setOpen(false);
window.electron.createChatWindow( window.electron.createChatWindow(
undefined, undefined,
window.appConfig.get('GOOSE_WORKING_DIR') window.appConfig.get('GOOSE_WORKING_DIR') as string | undefined
); );
}} }}
subtitle="Start a new session in the current directory" subtitle="Start a new session in the current directory"
@@ -244,7 +253,7 @@ export default function MoreMenu({
undefined, // dir undefined, // dir
undefined, // version undefined, // version
undefined, // resumeSessionId undefined, // resumeSessionId
recipeConfig, // recipe config recipeConfig as RecipeConfig, // recipe config
'recipeEditor' // view type 'recipeEditor' // view type
); );
}} }}

View File

@@ -44,7 +44,7 @@ export default function MoreMenuLayout({
> >
<Document className="mr-1" /> <Document className="mr-1" />
<div className="max-w-[200px] truncate [direction:rtl]"> <div className="max-w-[200px] truncate [direction:rtl]">
{window.appConfig.get('GOOSE_WORKING_DIR')} {String(window.appConfig.get('GOOSE_WORKING_DIR'))}
</div> </div>
</button> </button>
</TooltipTrigger> </TooltipTrigger>
@@ -54,7 +54,10 @@ export default function MoreMenuLayout({
</Tooltip> </Tooltip>
</TooltipProvider> </TooltipProvider>
<MoreMenu setView={setView} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} /> <MoreMenu
setView={setView || (() => {})}
setIsGoosehintsModalOpen={setIsGoosehintsModalOpen || (() => {})}
/>
</div> </div>
)} )}
</div> </div>

View File

@@ -96,12 +96,12 @@ function parseDeepLink(deepLink: string): Recipe | null {
if (url.protocol !== 'goose:' || (url.hostname !== 'bot' && url.hostname !== 'recipe')) { if (url.protocol !== 'goose:' || (url.hostname !== 'bot' && url.hostname !== 'recipe')) {
return null; return null;
} }
const configParam = url.searchParams.get('config'); const configParam = url.searchParams.get('config');
if (!configParam) { if (!configParam) {
return null; return null;
} }
const configJson = Buffer.from(configParam, 'base64').toString('utf-8'); const configJson = Buffer.from(configParam, 'base64').toString('utf-8');
return JSON.parse(configJson) as Recipe; return JSON.parse(configJson) as Recipe;
} catch (error) { } catch (error) {
@@ -128,89 +128,87 @@ function recipeToYaml(recipe: Recipe): string {
} }
if (recipe.extensions && recipe.extensions.length > 0) { if (recipe.extensions && recipe.extensions.length > 0) {
cleanRecipe.extensions = recipe.extensions.map(ext => { cleanRecipe.extensions = recipe.extensions.map((ext) => {
const cleanExt: CleanExtension = { const cleanExt: CleanExtension = {
name: ext.name, name: ext.name,
type: 'builtin', // Default type, will be overridden below type: 'builtin', // Default type, will be overridden below
}; };
// Handle different extension types // Handle different extension types using type assertions
if ('type' in ext && ext.type) { if ('type' in ext && ext.type) {
cleanExt.type = ext.type as CleanExtension['type']; cleanExt.type = ext.type as CleanExtension['type'];
// Add type-specific fields based on the ExtensionConfig union types // Use type assertions to access properties safely
switch (ext.type) { const extAny = ext as Record<string, unknown>;
case 'sse':
if ('uri' in ext && ext.uri) { if (ext.type === 'sse' && extAny.uri) {
cleanExt.uri = ext.uri as string; cleanExt.uri = extAny.uri as string;
} } else if (ext.type === 'stdio') {
break; if (extAny.cmd) {
case 'stdio': cleanExt.cmd = extAny.cmd as string;
if ('cmd' in ext && ext.cmd) { }
cleanExt.cmd = ext.cmd as string; if (extAny.args) {
} cleanExt.args = extAny.args as string[];
if ('args' in ext && ext.args) { }
cleanExt.args = ext.args as string[]; } else if (ext.type === 'builtin' && extAny.display_name) {
} cleanExt.display_name = extAny.display_name as string;
break; }
case 'builtin':
if ('display_name' in ext && ext.display_name) { // Handle frontend type separately to avoid TypeScript narrowing issues
cleanExt.display_name = ext.display_name as string; if ((ext.type as string) === 'frontend') {
} if (extAny.tools) {
break; cleanExt.tools = extAny.tools as unknown[];
case 'frontend': }
if ('tools' in ext && ext.tools) { if (extAny.instructions) {
cleanExt.tools = ext.tools as unknown[]; cleanExt.instructions = extAny.instructions as string;
} }
if ('instructions' in ext && ext.instructions) {
cleanExt.instructions = ext.instructions as string;
}
break;
} }
} else { } else {
// Fallback: try to infer type from available fields // Fallback: try to infer type from available fields
if ('cmd' in ext && ext.cmd) { const extAny = ext as Record<string, unknown>;
if (extAny.cmd) {
cleanExt.type = 'stdio'; cleanExt.type = 'stdio';
cleanExt.cmd = ext.cmd as string; cleanExt.cmd = extAny.cmd as string;
if ('args' in ext && ext.args) { if (extAny.args) {
cleanExt.args = ext.args as string[]; cleanExt.args = extAny.args as string[];
} }
} else if ('command' in ext && ext.command) { } else if (extAny.command) {
// Handle legacy 'command' field by converting to 'cmd' // Handle legacy 'command' field by converting to 'cmd'
cleanExt.type = 'stdio'; cleanExt.type = 'stdio';
cleanExt.cmd = ext.command as string; cleanExt.cmd = extAny.command as string;
} else if ('uri' in ext && ext.uri) { } else if (extAny.uri) {
cleanExt.type = 'sse'; cleanExt.type = 'sse';
cleanExt.uri = ext.uri as string; cleanExt.uri = extAny.uri as string;
} else if ('tools' in ext && ext.tools) { } else if (extAny.tools) {
cleanExt.type = 'frontend'; cleanExt.type = 'frontend';
cleanExt.tools = ext.tools as unknown[]; cleanExt.tools = extAny.tools as unknown[];
if ('instructions' in ext && ext.instructions) { if (extAny.instructions) {
cleanExt.instructions = ext.instructions as string; cleanExt.instructions = extAny.instructions as string;
} }
} else { } else {
// Default to builtin if we can't determine type // Default to builtin if we can't determine type
cleanExt.type = 'builtin'; cleanExt.type = 'builtin';
} }
} }
// Add common optional fields // Add common optional fields
if (ext.env_keys && ext.env_keys.length > 0) { if (ext.env_keys && ext.env_keys.length > 0) {
cleanExt.env_keys = ext.env_keys; cleanExt.env_keys = ext.env_keys;
} }
if ('timeout' in ext && ext.timeout) { if ('timeout' in ext && ext.timeout) {
cleanExt.timeout = ext.timeout as number; cleanExt.timeout = ext.timeout as number;
} }
if ('description' in ext && ext.description) { if ('description' in ext && ext.description) {
cleanExt.description = ext.description as string; cleanExt.description = ext.description as string;
} }
if ('bundled' in ext && ext.bundled !== undefined) { if ('bundled' in ext && ext.bundled !== undefined) {
cleanExt.bundled = ext.bundled as boolean; cleanExt.bundled = ext.bundled as boolean;
} }
return cleanExt; return cleanExt;
}); });
} }
@@ -258,27 +256,35 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
const [readableCronExpression, setReadableCronExpression] = useState<string>(''); const [readableCronExpression, setReadableCronExpression] = useState<string>('');
const [internalValidationError, setInternalValidationError] = useState<string | null>(null); const [internalValidationError, setInternalValidationError] = useState<string | null>(null);
const handleDeepLinkChange = useCallback((value: string) => { const handleDeepLinkChange = useCallback(
setDeepLinkInput(value); (value: string) => {
setInternalValidationError(null); setDeepLinkInput(value);
setInternalValidationError(null);
if (value.trim()) {
const recipe = parseDeepLink(value.trim()); if (value.trim()) {
if (recipe) { const recipe = parseDeepLink(value.trim());
setParsedRecipe(recipe); if (recipe) {
// Auto-populate schedule ID from recipe title if available setParsedRecipe(recipe);
if (recipe.title && !scheduleId) { // Auto-populate schedule ID from recipe title if available
const cleanId = recipe.title.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-'); if (recipe.title && !scheduleId) {
setScheduleId(cleanId); const cleanId = recipe.title
.toLowerCase()
.replace(/[^a-z0-9-]/g, '-')
.replace(/-+/g, '-');
setScheduleId(cleanId);
}
} else {
setParsedRecipe(null);
setInternalValidationError(
'Invalid deep link format. Please use a goose://bot or goose://recipe link.'
);
} }
} else { } else {
setParsedRecipe(null); setParsedRecipe(null);
setInternalValidationError('Invalid deep link format. Please use a goose://bot or goose://recipe link.');
} }
} else { },
setParsedRecipe(null); [scheduleId]
} );
}, [scheduleId]);
useEffect(() => { useEffect(() => {
// Check for pending deep link when modal opens // Check for pending deep link when modal opens
@@ -420,7 +426,7 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
} }
let finalRecipeSource = ''; let finalRecipeSource = '';
if (sourceType === 'file') { if (sourceType === 'file') {
if (!recipeSourcePath) { if (!recipeSourcePath) {
setInternalValidationError('Recipe source file is required.'); setInternalValidationError('Recipe source file is required.');
@@ -436,7 +442,7 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
setInternalValidationError('Invalid deep link. Please check the format.'); setInternalValidationError('Invalid deep link. Please check the format.');
return; return;
} }
try { try {
// Convert recipe to YAML and save to a temporary file // Convert recipe to YAML and save to a temporary file
const yamlContent = recipeToYaml(parsedRecipe); const yamlContent = recipeToYaml(parsedRecipe);
@@ -444,14 +450,14 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
const tempFileName = `schedule-${scheduleId}-${Date.now()}.yaml`; const tempFileName = `schedule-${scheduleId}-${Date.now()}.yaml`;
const tempDir = window.electron.getConfig().GOOSE_WORKING_DIR || '.'; const tempDir = window.electron.getConfig().GOOSE_WORKING_DIR || '.';
const tempFilePath = `${tempDir}/${tempFileName}`; const tempFilePath = `${tempDir}/${tempFileName}`;
// Write the YAML file // Write the YAML file
const writeSuccess = await window.electron.writeFile(tempFilePath, yamlContent); const writeSuccess = await window.electron.writeFile(tempFilePath, yamlContent);
if (!writeSuccess) { if (!writeSuccess) {
setInternalValidationError('Failed to create temporary recipe file.'); setInternalValidationError('Failed to create temporary recipe file.');
return; return;
} }
finalRecipeSource = tempFilePath; finalRecipeSource = tempFilePath;
} catch (error) { } catch (error) {
console.error('Failed to convert recipe to YAML:', error); console.error('Failed to convert recipe to YAML:', error);
@@ -610,7 +616,8 @@ export const CreateScheduleModal: React.FC<CreateScheduleModalProps> = ({
instanceId="frequency-select-modal" instanceId="frequency-select-modal"
options={frequencies} options={frequencies}
value={frequencies.find((f) => f.value === frequency)} value={frequencies.find((f) => f.value === frequency)}
onChange={(selectedOption: FrequencyOption | null) => { onChange={(newValue: unknown) => {
const selectedOption = newValue as FrequencyOption | null;
if (selectedOption) setFrequency(selectedOption.value); if (selectedOption) setFrequency(selectedOption.value);
}} }}
placeholder="Select frequency..." placeholder="Select frequency..."

View File

@@ -58,16 +58,40 @@ const parseCronExpression = (cron: string) => {
if (dayOfMonth !== '*' && month !== '*' && dayOfWeek === '*') { if (dayOfMonth !== '*' && month !== '*' && dayOfWeek === '*') {
return { frequency: 'once' as FrequencyValue, minutes, hours, dayOfMonth, month }; return { frequency: 'once' as FrequencyValue, minutes, hours, dayOfMonth, month };
} }
if (minutes !== '*' && hours === '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') { if (
minutes !== '*' &&
hours === '*' &&
dayOfMonth === '*' &&
month === '*' &&
dayOfWeek === '*'
) {
return { frequency: 'hourly' as FrequencyValue, minutes }; return { frequency: 'hourly' as FrequencyValue, minutes };
} }
if (minutes !== '*' && hours !== '*' && dayOfMonth === '*' && month === '*' && dayOfWeek === '*') { if (
minutes !== '*' &&
hours !== '*' &&
dayOfMonth === '*' &&
month === '*' &&
dayOfWeek === '*'
) {
return { frequency: 'daily' as FrequencyValue, minutes, hours }; return { frequency: 'daily' as FrequencyValue, minutes, hours };
} }
if (minutes !== '*' && hours !== '*' && dayOfMonth === '*' && month === '*' && dayOfWeek !== '*') { if (
minutes !== '*' &&
hours !== '*' &&
dayOfMonth === '*' &&
month === '*' &&
dayOfWeek !== '*'
) {
return { frequency: 'weekly' as FrequencyValue, minutes, hours, dayOfWeek }; return { frequency: 'weekly' as FrequencyValue, minutes, hours, dayOfWeek };
} }
if (minutes !== '*' && hours !== '*' && dayOfMonth !== '*' && month === '*' && dayOfWeek === '*') { if (
minutes !== '*' &&
hours !== '*' &&
dayOfMonth !== '*' &&
month === '*' &&
dayOfWeek === '*'
) {
return { frequency: 'monthly' as FrequencyValue, minutes, hours, dayOfMonth }; return { frequency: 'monthly' as FrequencyValue, minutes, hours, dayOfMonth };
} }
@@ -98,32 +122,40 @@ export const EditScheduleModal: React.FC<EditScheduleModalProps> = ({
useEffect(() => { useEffect(() => {
if (schedule && isOpen) { if (schedule && isOpen) {
const parsed = parseCronExpression(schedule.cron); const parsed = parseCronExpression(schedule.cron);
if (parsed) { if (parsed) {
setFrequency(parsed.frequency); setFrequency(parsed.frequency);
switch (parsed.frequency) { switch (parsed.frequency) {
case 'once': case 'once':
// For 'once', we'd need to reconstruct the date from cron parts // For 'once', we'd need to reconstruct the date from cron parts
// This is complex, so we'll default to current date/time for now // This is complex, so we'll default to current date/time for now
setSelectedDate(new Date().toISOString().split('T')[0]); setSelectedDate(new Date().toISOString().split('T')[0]);
setSelectedTime(`${parsed.hours?.padStart(2, '0')}:${parsed.minutes?.padStart(2, '0')}`); setSelectedTime(
`${parsed.hours?.padStart(2, '0')}:${parsed.minutes?.padStart(2, '0')}`
);
break; break;
case 'hourly': case 'hourly':
setSelectedMinute(parsed.minutes || '0'); setSelectedMinute(parsed.minutes || '0');
break; break;
case 'daily': case 'daily':
setSelectedTime(`${parsed.hours?.padStart(2, '0')}:${parsed.minutes?.padStart(2, '0')}`); setSelectedTime(
`${parsed.hours?.padStart(2, '0')}:${parsed.minutes?.padStart(2, '0')}`
);
break; break;
case 'weekly': case 'weekly':
setSelectedTime(`${parsed.hours?.padStart(2, '0')}:${parsed.minutes?.padStart(2, '0')}`); setSelectedTime(
`${parsed.hours?.padStart(2, '0')}:${parsed.minutes?.padStart(2, '0')}`
);
if (parsed.dayOfWeek) { if (parsed.dayOfWeek) {
const days = parsed.dayOfWeek.split(',').map(d => d.trim()); const days = parsed.dayOfWeek.split(',').map((d) => d.trim());
setSelectedDaysOfWeek(new Set(days)); setSelectedDaysOfWeek(new Set(days));
} }
break; break;
case 'monthly': case 'monthly':
setSelectedTime(`${parsed.hours?.padStart(2, '0')}:${parsed.minutes?.padStart(2, '0')}`); setSelectedTime(
`${parsed.hours?.padStart(2, '0')}:${parsed.minutes?.padStart(2, '0')}`
);
setSelectedDayOfMonth(parsed.dayOfMonth || '1'); setSelectedDayOfMonth(parsed.dayOfMonth || '1');
break; break;
} }
@@ -132,7 +164,7 @@ export const EditScheduleModal: React.FC<EditScheduleModalProps> = ({
setFrequency('daily'); setFrequency('daily');
setSelectedTime('09:00'); setSelectedTime('09:00');
} }
setInternalValidationError(null); setInternalValidationError(null);
} }
}, [schedule, isOpen]); }, [schedule, isOpen]);
@@ -287,7 +319,8 @@ export const EditScheduleModal: React.FC<EditScheduleModalProps> = ({
instanceId="frequency-select-modal" instanceId="frequency-select-modal"
options={frequencies} options={frequencies}
value={frequencies.find((f) => f.value === frequency)} value={frequencies.find((f) => f.value === frequency)}
onChange={(selectedOption: FrequencyOption | null) => { onChange={(newValue: unknown) => {
const selectedOption = newValue as FrequencyOption | null;
if (selectedOption) setFrequency(selectedOption.value); if (selectedOption) setFrequency(selectedOption.value);
}} }}
placeholder="Select frequency..." placeholder="Select frequency..."
@@ -431,4 +464,4 @@ export const EditScheduleModal: React.FC<EditScheduleModalProps> = ({
</Card> </Card>
</div> </div>
); );
}; };

View File

@@ -7,7 +7,11 @@ import { ScrollArea } from '../ui/scroll-area';
import MarkdownContent from '../MarkdownContent'; import MarkdownContent from '../MarkdownContent';
import ToolCallWithResponse from '../ToolCallWithResponse'; import ToolCallWithResponse from '../ToolCallWithResponse';
import ImagePreview from '../ImagePreview'; import ImagePreview from '../ImagePreview';
import { ToolRequestMessageContent, ToolResponseMessageContent } from '../../types/message'; import {
ToolRequestMessageContent,
ToolResponseMessageContent,
TextContent,
} from '../../types/message';
import { type Message } from '../../types/message'; import { type Message } from '../../types/message';
import { formatMessageTimestamp } from '../../utils/timeUtils'; import { formatMessageTimestamp } from '../../utils/timeUtils';
import { extractImagePaths, removeImagePathsFromText } from '../../utils/imageUtils'; import { extractImagePaths, removeImagePathsFromText } from '../../utils/imageUtils';
@@ -109,7 +113,7 @@ export const SessionMessages: React.FC<SessionMessagesProps> = ({
.map((message, index) => { .map((message, index) => {
// Extract text content from the message // Extract text content from the message
let textContent = message.content let textContent = message.content
.filter((c) => c.type === 'text') .filter((c): c is TextContent => c.type === 'text')
.map((c) => c.text) .map((c) => c.text)
.join('\n'); .join('\n');

View File

@@ -33,13 +33,13 @@ const SharedSessionView: React.FC<SharedSessionViewProps> = ({
<div className="flex items-center text-sm text-textSubtle mt-1 space-x-5"> <div className="flex items-center text-sm text-textSubtle mt-1 space-x-5">
<span className="flex items-center"> <span className="flex items-center">
<Calendar className="w-4 h-4 mr-1" /> <Calendar className="w-4 h-4 mr-1" />
{formatMessageTimestamp(session.messages[0]?.created)} {session ? formatMessageTimestamp(session.messages[0]?.created) : 'Unknown'}
</span> </span>
<span className="flex items-center"> <span className="flex items-center">
<MessageSquareText className="w-4 h-4 mr-1" /> <MessageSquareText className="w-4 h-4 mr-1" />
{session.message_count} {session ? session.message_count : 0}
</span> </span>
{session.total_tokens !== null && ( {session && session.total_tokens !== null && (
<span className="flex items-center"> <span className="flex items-center">
<Target className="w-4 h-4 mr-1" /> <Target className="w-4 h-4 mr-1" />
{session.total_tokens.toLocaleString()} {session.total_tokens.toLocaleString()}
@@ -49,7 +49,7 @@ const SharedSessionView: React.FC<SharedSessionViewProps> = ({
<div className="flex items-center text-sm text-textSubtle space-x-5"> <div className="flex items-center text-sm text-textSubtle space-x-5">
<span className="flex items-center"> <span className="flex items-center">
<Folder className="w-4 h-4 mr-1" /> <Folder className="w-4 h-4 mr-1" />
{session.working_dir} {session ? session.working_dir : 'Unknown'}
</span> </span>
</div> </div>
</div> </div>

View File

@@ -404,8 +404,12 @@ export function OllamaBattleGame({ onComplete, requiredKeys: _ }: OllamaBattleGa
!battleState.processingAction && ( !battleState.processingAction && (
<div className="space-y-2"> <div className="space-y-2">
{(typeof battleSteps[battleState.currentStep].choices === 'function' {(typeof battleSteps[battleState.currentStep].choices === 'function'
? (battleSteps[battleState.currentStep].choices as (choice: string) => string[])(battleState.lastChoice || '') ? (
: battleSteps[battleState.currentStep].choices as string[] battleSteps[battleState.currentStep].choices as (
choice: string
) => string[]
)(battleState.lastChoice || '')
: (battleSteps[battleState.currentStep].choices as string[])
)?.map((choice: string) => ( )?.map((choice: string) => (
<button <button
key={choice} key={choice}

View File

@@ -29,7 +29,7 @@ export function ProviderSetupModal({
const [configValues, setConfigValues] = React.useState<{ [key: string]: string }>( const [configValues, setConfigValues] = React.useState<{ [key: string]: string }>(
default_key_value default_key_value
); );
const requiredKeys = required_keys[provider] || ['API Key']; const requiredKeys = (required_keys as Record<string, string[]>)[provider] || ['API Key'];
const headerText = title || `Setup ${provider}`; const headerText = title || `Setup ${provider}`;
const shouldShowBattle = React.useMemo(() => { const shouldShowBattle = React.useMemo(() => {
@@ -59,7 +59,7 @@ export function ProviderSetupModal({
) : ( ) : (
<form onSubmit={handleSubmit}> <form onSubmit={handleSubmit}>
<div className="mt-[24px] space-y-4"> <div className="mt-[24px] space-y-4">
{requiredKeys.map((keyName) => ( {requiredKeys.map((keyName: string) => (
<div key={keyName}> <div key={keyName}>
<Input <Input
type={isSecretKey(keyName) ? 'password' : 'text'} type={isSecretKey(keyName) ? 'password' : 'text'}

View File

@@ -46,7 +46,6 @@ const DEFAULT_SETTINGS: SettingsType = {
enabled: true, enabled: true,
}, },
], ],
// @ts-expect-error "we actually do always have all the properties required for builtins, but tsc cannot tell for some reason"
extensions: BUILT_IN_EXTENSIONS, extensions: BUILT_IN_EXTENSIONS,
}; };

View File

@@ -40,10 +40,10 @@ export async function getActiveProviders(): Promise<string[]> {
const configStatus = provider.config_status ?? {}; const configStatus = provider.config_status ?? {};
// Skip if provider isn't in required_keys // Skip if provider isn't in required_keys
if (!required_keys[providerName]) return false; if (!required_keys[providerName as keyof typeof required_keys]) return false;
// Get all required keys for this provider // Get all required keys for this provider
const providerRequiredKeys = required_keys[providerName]; const providerRequiredKeys = required_keys[providerName as keyof typeof required_keys];
// Special case: If a provider has exactly one required key and that key // Special case: If a provider has exactly one required key and that key
// has a default value, check if it's explicitly set // has a default value, check if it's explicitly set
@@ -103,14 +103,17 @@ export async function getConfigSettings(): Promise<Record<string, ProviderRespon
supported: true, supported: true,
description: provider.metadata.description, description: provider.metadata.description,
models: provider.metadata.models, models: provider.metadata.models,
config_status: providerRequiredKeys.reduce<Record<string, ConfigDetails>>((acc: Record<string, ConfigDetails>, key: string) => { config_status: providerRequiredKeys.reduce<Record<string, ConfigDetails>>(
acc[key] = { (acc: Record<string, ConfigDetails>, key: string) => {
key, acc[key] = {
is_set: provider.is_configured, key,
location: provider.is_configured ? 'config' : undefined, is_set: provider.is_configured,
}; location: provider.is_configured ? 'config' : undefined,
return acc; };
}, {}), return acc;
},
{}
),
}; };
}); });

View File

@@ -39,7 +39,7 @@ export function ConfigureApproveMode({
setIsSubmitting(true); setIsSubmitting(true);
try { try {
handleModeChange(approveMode); handleModeChange(approveMode || '');
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Error configuring goose mode:', error); console.error('Error configuring goose mode:', error);
@@ -71,7 +71,7 @@ export function ConfigureApproveMode({
key={mode.key} key={mode.key}
mode={mode} mode={mode}
showDescription={true} showDescription={true}
currentMode={approveMode} currentMode={approveMode || ''}
isApproveModeConfigure={true} isApproveModeConfigure={true}
handleModeChange={(newMode) => { handleModeChange={(newMode) => {
setApproveMode(newMode); setApproveMode(newMode);

View File

@@ -38,7 +38,7 @@ export function ConfigureBuiltInExtensionModal({
setIsSubmitting(true); setIsSubmitting(true);
try { try {
// First store all environment variables // First store all environment variables
if (extension.env_keys?.length > 0) { if (extension.env_keys && extension.env_keys.length > 0) {
for (const envKey of extension.env_keys) { for (const envKey of extension.env_keys) {
const value = envValues[envKey]; const value = envValues[envKey];
if (!value) continue; if (!value) continue;
@@ -103,13 +103,13 @@ export function ConfigureBuiltInExtensionModal({
{/* Form */} {/* Form */}
<form onSubmit={handleExtensionConfigSubmit}> <form onSubmit={handleExtensionConfigSubmit}>
<div className="mt-[24px]"> <div className="mt-[24px]">
{extension.env_keys?.length > 0 ? ( {extension.env_keys && extension.env_keys.length > 0 ? (
<> <>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6"> <p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
Please provide the required environment variables for this extension: Please provide the required environment variables for this extension:
</p> </p>
<div className="space-y-4"> <div className="space-y-4">
{extension.env_keys?.map((envVarName) => ( {extension.env_keys.map((envVarName) => (
<div key={envVarName}> <div key={envVarName}>
<label <label
htmlFor={envVarName} htmlFor={envVarName}

View File

@@ -40,7 +40,7 @@ export function ConfigureExtensionModal({
setIsSubmitting(true); setIsSubmitting(true);
try { try {
// First store all environment variables // First store all environment variables
if (extension.env_keys?.length > 0) { if (extension.env_keys && extension.env_keys.length > 0) {
for (const envKey of extension.env_keys) { for (const envKey of extension.env_keys) {
const value = envValues[envKey]; const value = envValues[envKey];
if (!value) continue; if (!value) continue;
@@ -105,13 +105,13 @@ export function ConfigureExtensionModal({
{/* Form */} {/* Form */}
<form onSubmit={handleExtensionConfigSubmit}> <form onSubmit={handleExtensionConfigSubmit}>
<div className="mt-[24px]"> <div className="mt-[24px]">
{extension.env_keys?.length > 0 ? ( {extension.env_keys && extension.env_keys.length > 0 ? (
<> <>
<p className="text-sm text-gray-500 dark:text-gray-400 mb-6"> <p className="text-sm text-gray-500 dark:text-gray-400 mb-6">
Please provide the required environment variables for this extension: Please provide the required environment variables for this extension:
</p> </p>
<div className="space-y-4"> <div className="space-y-4">
{extension.env_keys?.map((envVarName) => ( {extension.env_keys.map((envVarName) => (
<div key={envVarName}> <div key={envVarName}>
<label <label
htmlFor={envVarName} htmlFor={envVarName}

View File

@@ -97,7 +97,10 @@ export function ManualExtensionModal({ isOpen, onClose, onSubmit }: ManualExtens
resetForm(); resetForm();
} catch (error) { } catch (error) {
console.error('Error configuring extension:', error); console.error('Error configuring extension:', error);
toastError({ title: 'Failed to configure extension', traceback: error.message }); toastError({
title: 'Failed to configure extension',
traceback: error instanceof Error ? error.message : String(error),
});
} }
}; };
@@ -142,9 +145,13 @@ export function ManualExtensionModal({ isOpen, onClose, onSubmit }: ManualExtens
<Select <Select
options={typeOptions} options={typeOptions}
value={typeOptions.find((option) => option.value === formData.type)} value={typeOptions.find((option) => option.value === formData.type)}
onChange={(option: { value: string; label: string } | null) => onChange={(newValue: unknown) => {
setFormData({ ...formData, type: option?.value as FullExtensionConfig['type'] }) const option = newValue as { value: string; label: string } | null;
} setFormData({
...formData,
type: option?.value as FullExtensionConfig['type'],
});
}}
/> />
</div> </div>

View File

@@ -19,7 +19,9 @@ export function AddModelInline() {
const [selectedProvider, setSelectedProvider] = useState<string | null>(null); const [selectedProvider, setSelectedProvider] = useState<string | null>(null);
const [modelName, setModelName] = useState<string>(''); const [modelName, setModelName] = useState<string>('');
const [filteredModels, setFilteredModels] = useState<{ id: string; name: string; provider: string }[]>([]); const [filteredModels, setFilteredModels] = useState<
{ id: string; name: string; provider: string }[]
>([]);
const [showSuggestions, setShowSuggestions] = useState(false); const [showSuggestions, setShowSuggestions] = useState(false);
const handleModelSelection = useHandleModelSelection(); const handleModelSelection = useHandleModelSelection();
@@ -37,7 +39,12 @@ export function AddModelInline() {
model.provider.toLowerCase() === selectedProvider && model.provider.toLowerCase() === selectedProvider &&
model.name.toLowerCase().includes(modelName.toLowerCase()) model.name.toLowerCase().includes(modelName.toLowerCase())
) )
.slice(0, 5); // Limit suggestions to top 5 .slice(0, 5) // Limit suggestions to top 5
.map((model) => ({
id: String(model.id || ''),
name: model.name,
provider: model.provider,
}));
setFilteredModels(filtered); setFilteredModels(filtered);
setShowSuggestions(filtered.length > 0); setShowSuggestions(filtered.length > 0);
}, [modelName, selectedProvider]); }, [modelName, selectedProvider]);
@@ -76,7 +83,8 @@ export function AddModelInline() {
<Select <Select
options={providerOptions} options={providerOptions}
value={providerOptions.find((option) => option.value === selectedProvider) || null} value={providerOptions.find((option) => option.value === selectedProvider) || null}
onChange={(option: { value: string | null }) => { onChange={(newValue: unknown) => {
const option = newValue as { value: string | null } | null;
setSelectedProvider(option?.value || null); setSelectedProvider(option?.value || null);
setModelName(''); // Clear model name when provider changes setModelName(''); // Clear model name when provider changes
setFilteredModels([]); setFilteredModels([]);

View File

@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
import { useRecentModels } from './RecentModels'; import { useRecentModels } from './RecentModels';
import { useModel, Model } from './ModelContext'; import { useModel, Model } from './ModelContext';
import { useHandleModelSelection } from './utils'; import { useHandleModelSelection } from './utils';
import type { View } from '@/src/App'; import type { View } from '../../../App';
interface ModelRadioListProps { interface ModelRadioListProps {
renderItem: (props: { renderItem: (props: {

View File

@@ -8,10 +8,13 @@ import { useModel } from './ModelContext';
import { useHandleModelSelection } from './utils'; import { useHandleModelSelection } from './utils';
// Create a mapping from provider name to href // Create a mapping from provider name to href
const providerLinks = model_docs_link.reduce((acc, { name, href }) => { const providerLinks: Record<string, string> = model_docs_link.reduce(
acc[name] = href; (acc, { name, href }) => {
return acc; acc[name] = href;
}, {}); return acc;
},
{} as Record<string, string>
);
export function ProviderButtons() { export function ProviderButtons() {
const { activeKeys } = useActiveKeys(); const { activeKeys } = useActiveKeys();

View File

@@ -43,7 +43,7 @@ export function useHandleModelSelection() {
toastError({ toastError({
title: model.name, title: model.name,
msg: `Failed to switch to model`, msg: `Failed to switch to model`,
traceback: error.message, traceback: error instanceof Error ? error.message : String(error),
}); });
} }
}; };

View File

@@ -62,7 +62,7 @@ function BaseProviderCard({
onTakeoff, onTakeoff,
showTakeoff, showTakeoff,
}: BaseProviderCardProps) { }: BaseProviderCardProps) {
const numRequiredKeys = required_keys[name]?.length || 0; const numRequiredKeys = (required_keys as Record<string, string[]>)[name]?.length || 0;
const tooltipText = numRequiredKeys === 1 ? `Add ${name} API Key` : `Add ${name} API Keys`; const tooltipText = numRequiredKeys === 1 ? `Add ${name} API Key` : `Add ${name} API Keys`;
return ( return (
@@ -254,7 +254,8 @@ export function BaseProviderGrid({
return ( return (
<div className="grid grid-cols-[repeat(auto-fill,_minmax(140px,_1fr))] gap-3 [&_*]:z-20"> <div className="grid grid-cols-[repeat(auto-fill,_minmax(140px,_1fr))] gap-3 [&_*]:z-20">
{providers.map((provider) => { {providers.map((provider) => {
const hasRequiredKeys = required_keys[provider.name]?.length > 0; const hasRequiredKeys =
(required_keys as Record<string, string[]>)[provider.name]?.length > 0;
return ( return (
<BaseProviderCard <BaseProviderCard
key={provider.id} key={provider.id}

View File

@@ -9,7 +9,15 @@ import { useModel } from '../models/ModelContext';
import { Button } from '../../ui/button'; import { Button } from '../../ui/button';
import { toastError, toastSuccess } from '../../../toasts'; import { toastError, toastSuccess } from '../../../toasts';
function ConfirmationModal({ message, onConfirm, onCancel }) { function ConfirmationModal({
message,
onConfirm,
onCancel,
}: {
message: string;
onConfirm: () => void;
onCancel: () => void;
}) {
return ( return (
<div className="fixed inset-0 bg-black/20 backdrop-blur-sm z-[9999]"> <div className="fixed inset-0 bg-black/20 backdrop-blur-sm z-[9999]">
<div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700"> <div className="fixed top-1/2 left-1/2 -translate-x-1/2 -translate-y-1/2 w-[400px] bg-white dark:bg-gray-800 rounded-xl shadow-xl border border-gray-200 dark:border-gray-700">
@@ -43,7 +51,12 @@ export function ConfigureProvidersGrid() {
const [selectedForSetup, setSelectedForSetup] = useState<string | null>(null); const [selectedForSetup, setSelectedForSetup] = useState<string | null>(null);
const [modalMode, setModalMode] = useState<'edit' | 'setup' | 'battle'>('setup'); const [modalMode, setModalMode] = useState<'edit' | 'setup' | 'battle'>('setup');
const [isConfirmationOpen, setIsConfirmationOpen] = useState(false); const [isConfirmationOpen, setIsConfirmationOpen] = useState(false);
const [providerToDelete, setProviderToDelete] = useState<{ name: string; id: string; isConfigured: boolean; description: string } | null>(null); const [providerToDelete, setProviderToDelete] = useState<{
name: string;
id: string;
isConfigured: boolean;
description: string;
} | null>(null);
const { currentModel } = useModel(); const { currentModel } = useModel();
const providers = useMemo(() => { const providers = useMemo(() => {
@@ -62,13 +75,23 @@ export function ConfigureProvidersGrid() {
}); });
}, [activeKeys]); }, [activeKeys]);
const handleAddKeys = (provider) => { const handleAddKeys = (provider: {
id: string;
name: string;
isConfigured: boolean;
description: string;
}) => {
setSelectedForSetup(provider.id); setSelectedForSetup(provider.id);
setModalMode('setup'); setModalMode('setup');
setShowSetupModal(true); setShowSetupModal(true);
}; };
const handleConfigure = (provider) => { const handleConfigure = (provider: {
id: string;
name: string;
isConfigured: boolean;
description: string;
}) => {
setSelectedForSetup(provider.id); setSelectedForSetup(provider.id);
setModalMode('edit'); setModalMode('edit');
setShowSetupModal(true); setShowSetupModal(true);
@@ -80,7 +103,7 @@ export function ConfigureProvidersGrid() {
const provider = providers.find((p) => p.id === selectedForSetup)?.name; const provider = providers.find((p) => p.id === selectedForSetup)?.name;
if (!provider) return; if (!provider) return;
const requiredKeys = required_keys[provider]; const requiredKeys = (required_keys as Record<string, string[]>)[provider];
if (!requiredKeys || requiredKeys.length === 0) { if (!requiredKeys || requiredKeys.length === 0) {
console.error(`No keys found for provider ${provider}`); console.error(`No keys found for provider ${provider}`);
return; return;
@@ -157,19 +180,24 @@ export function ConfigureProvidersGrid() {
toastError({ toastError({
title: provider, title: provider,
msg: `Failed to ${providers.find((p) => p.id === selectedForSetup)?.isConfigured ? 'update' : 'add'} configuration`, msg: `Failed to ${providers.find((p) => p.id === selectedForSetup)?.isConfigured ? 'update' : 'add'} configuration`,
traceback: error.message, traceback: error instanceof Error ? error.message : String(error),
}); });
} }
}; };
const handleDelete = async (provider) => { const handleDelete = async (provider: {
id: string;
name: string;
isConfigured: boolean;
description: string;
}) => {
setProviderToDelete(provider); setProviderToDelete(provider);
setIsConfirmationOpen(true); setIsConfirmationOpen(true);
}; };
const confirmDelete = async () => { const confirmDelete = async () => {
if (!providerToDelete) return; if (!providerToDelete) return;
const requiredKeys = required_keys[providerToDelete.name as keyof typeof required_keys]; const requiredKeys = required_keys[providerToDelete.name as keyof typeof required_keys];
if (!requiredKeys || requiredKeys.length === 0) { if (!requiredKeys || requiredKeys.length === 0) {
console.error(`No keys found for provider ${providerToDelete.name}`); console.error(`No keys found for provider ${providerToDelete.name}`);
@@ -220,7 +248,7 @@ export function ConfigureProvidersGrid() {
toastError({ toastError({
title: providerToDelete.name, title: providerToDelete.name,
msg: 'Failed to delete configuration', msg: 'Failed to delete configuration',
traceback: error.message, traceback: error instanceof Error ? error.message : String(error),
}); });
} }
setIsConfirmationOpen(false); setIsConfirmationOpen(false);

View File

@@ -3,7 +3,7 @@ import { Input } from '../../ui/input';
import { Check, Lock } from 'lucide-react'; import { Check, Lock } from 'lucide-react';
export default function SessionSharingSection() { export default function SessionSharingSection() {
const envBaseUrlShare = window.appConfig.get('GOOSE_BASE_URL_SHARE'); const envBaseUrlShare = window.appConfig.get('GOOSE_BASE_URL_SHARE') as string | undefined;
console.log('envBaseUrlShare', envBaseUrlShare); console.log('envBaseUrlShare', envBaseUrlShare);
// If env is set, force sharing enabled and set the baseUrl accordingly. // If env is set, force sharing enabled and set the baseUrl accordingly.
@@ -146,7 +146,7 @@ export default function SessionSharingSection() {
placeholder="https://example.com/api" placeholder="https://example.com/api"
value={sessionSharingConfig.baseUrl} value={sessionSharingConfig.baseUrl}
disabled={!!envBaseUrlShare} disabled={!!envBaseUrlShare}
onChange={envBaseUrlShare ? undefined : handleBaseUrlChange} onChange={envBaseUrlShare ? () => {} : handleBaseUrlChange}
/> />
</div> </div>
{urlError && <p className="text-red-500 text-sm">{urlError}</p>} {urlError && <p className="text-red-500 text-sm">{urlError}</p>}

View File

@@ -250,7 +250,10 @@ export default function ExtensionsSection({
{deepLinkConfigStateVar && showEnvVarsStateVar && ( {deepLinkConfigStateVar && showEnvVarsStateVar && (
<ExtensionModal <ExtensionModal
title="Add custom extension" title="Add custom extension"
initialData={extensionToFormData({ ...deepLinkConfig, enabled: true })} initialData={extensionToFormData({
...deepLinkConfig,
enabled: true,
} as FixedExtensionEntry)}
onClose={handleModalClose} onClose={handleModalClose}
onSubmit={handleAddExtension} onSubmit={handleAddExtension}
submitLabel="Add Extension" submitLabel="Add Extension"

View File

@@ -29,7 +29,7 @@ export async function extensionApiCall(
}; };
// for adding the payload is an extensionConfig, for removing payload is just the name // for adding the payload is an extensionConfig, for removing payload is just the name
const extensionName = isActivating ? (payload as ExtensionConfig).name : payload as string; const extensionName = isActivating ? (payload as ExtensionConfig).name : (payload as string);
let toastId; let toastId;
// Step 1: Show loading toast (only for activation of stdio) // Step 1: Show loading toast (only for activation of stdio)
@@ -77,11 +77,13 @@ export async function extensionApiCall(
} catch (error) { } catch (error) {
// Final catch-all error handler // Final catch-all error handler
toastService.dismiss(toastId); toastService.dismiss(toastId);
const msg = error.length < 70 ? error : `Failed to ${action.presentTense} extension`; const errorMessage = error instanceof Error ? error.message : String(error);
const msg =
errorMessage.length < 70 ? errorMessage : `Failed to ${action.presentTense} extension`;
toastService.error({ toastService.error({
title: extensionName, title: extensionName,
msg: msg, msg: msg,
traceback: error, traceback: errorMessage,
}); });
console.error(`Error in extensionApiCall for ${extensionName}:`, error); console.error(`Error in extensionApiCall for ${extensionName}:`, error);
throw error; throw error;
@@ -95,7 +97,7 @@ function handleErrorResponse(
response: Response, response: Response,
extensionName: string, extensionName: string,
action: { type: string; verb: string }, action: { type: string; verb: string },
toastId: string toastId: string | number | undefined
): never { ): never {
const errorMsg = `Server returned ${response.status}: ${response.statusText}`; const errorMsg = `Server returned ${response.status}: ${response.statusText}`;
console.error(errorMsg); console.error(errorMsg);
@@ -150,7 +152,7 @@ export async function addToAgent(
return await extensionApiCall('/extensions/add', extension, options); return await extensionApiCall('/extensions/add', extension, options);
} catch (error) { } catch (error) {
// Check if this is a 428 error and make the message more descriptive // Check if this is a 428 error and make the message more descriptive
if (error.message && error.message.includes('428')) { if (error instanceof Error && error.message && error.message.includes('428')) {
const enhancedError = new Error( const enhancedError = new Error(
'Failed to add extension. Goose Agent was still starting up. Please try again.' 'Failed to add extension. Goose Agent was still starting up. Please try again.'
); );

View File

@@ -63,10 +63,10 @@ export async function syncBundledExtensions(
description: bundledExt.description, description: bundledExt.description,
type: bundledExt.type, type: bundledExt.type,
timeout: bundledExt.timeout, timeout: bundledExt.timeout,
cmd: bundledExt.cmd, cmd: bundledExt.cmd || '',
args: bundledExt.args, args: bundledExt.args || [],
envs: bundledExt.envs, envs: bundledExt.envs,
env_keys: bundledExt.env_keys, env_keys: bundledExt.env_keys || [],
bundled: true, bundled: true,
}; };
break; break;
@@ -76,7 +76,7 @@ export async function syncBundledExtensions(
description: bundledExt.description, description: bundledExt.description,
type: bundledExt.type, type: bundledExt.type,
timeout: bundledExt.timeout, timeout: bundledExt.timeout,
uri: bundledExt.uri, uri: bundledExt.uri || '',
bundled: true, bundled: true,
}; };
} }

View File

@@ -122,8 +122,8 @@ export async function addExtensionFromDeepLink(
const remoteUrl = parsedUrl.searchParams.get('url'); const remoteUrl = parsedUrl.searchParams.get('url');
const config = remoteUrl const config = remoteUrl
? getSseConfig(remoteUrl, name, description, timeout) ? getSseConfig(remoteUrl, name, description || '', timeout)
: getStdioConfig(cmd!, parsedUrl, name, description, timeout); : getStdioConfig(cmd!, parsedUrl, name, description || '', timeout);
// Check if extension requires env vars and go to settings if so // Check if extension requires env vars and go to settings if so
if (config.envs && Object.keys(config.envs).length > 0) { if (config.envs && Object.keys(config.envs).length > 0) {

View File

@@ -25,7 +25,7 @@ async function retryWithBackoff<T>(fn: () => Promise<T>, options: RetryOptions =
const { retries = 3, delayMs = 1000, backoffFactor = 1.5, shouldRetry = () => true } = options; const { retries = 3, delayMs = 1000, backoffFactor = 1.5, shouldRetry = () => true } = options;
let attempt = 0; let attempt = 0;
let lastError: ExtensionError; let lastError: ExtensionError = new Error('Unknown error');
while (attempt <= retries) { while (attempt <= retries) {
try { try {
@@ -100,7 +100,7 @@ export async function addToAgentOnStartup({
retries: 3, retries: 3,
delayMs: 1000, delayMs: 1000,
shouldRetry: (error: ExtensionError) => shouldRetry: (error: ExtensionError) =>
error.message && !!error.message &&
(error.message.includes('428') || (error.message.includes('428') ||
error.message.includes('Precondition Required') || error.message.includes('Precondition Required') ||
error.message.includes('Agent is not initialized')), error.message.includes('Agent is not initialized')),
@@ -110,7 +110,7 @@ export async function addToAgentOnStartup({
toastService.error({ toastService.error({
title: extensionConfig.name, title: extensionConfig.name,
msg: 'Extension failed to start and will be disabled.', msg: 'Extension failed to start and will be disabled.',
traceback: finalError as Error, traceback: finalError instanceof Error ? finalError.message : String(finalError),
}); });
try { try {

View File

@@ -44,7 +44,8 @@ export default function ExtensionInfoFields({
<label className="text-sm font-medium mb-2 block text-textStandard">Type</label> <label className="text-sm font-medium mb-2 block text-textStandard">Type</label>
<Select <Select
value={{ value: type, label: type.toUpperCase() }} value={{ value: type, label: type.toUpperCase() }}
onChange={(option: { value: string; label: string } | null) => { onChange={(newValue: unknown) => {
const option = newValue as { value: string; label: string } | null;
if (option) { if (option) {
onChange('type', option.value); onChange('type', option.value);
} }

View File

@@ -98,8 +98,8 @@ export default function ExtensionModal({
const isConfigValid = () => { const isConfigValid = () => {
return ( return (
(formData.type === 'stdio' && formData.cmd && formData.cmd.trim() !== '') || (formData.type === 'stdio' && !!formData.cmd && formData.cmd.trim() !== '') ||
(formData.type === 'sse' && formData.endpoint && formData.endpoint.trim() !== '') (formData.type === 'sse' && !!formData.endpoint && formData.endpoint.trim() !== '')
); );
}; };
@@ -263,7 +263,7 @@ export default function ExtensionModal({
/> />
<div className="mb-4" /> <div className="mb-4" />
<ExtensionTimeoutField <ExtensionTimeoutField
timeout={formData.timeout} timeout={formData.timeout || 300}
onChange={(key, value) => setFormData({ ...formData, [key]: value })} onChange={(key, value) => setFormData({ ...formData, [key]: value })}
submitAttempted={submitAttempted} submitAttempted={submitAttempted}
/> />

View File

@@ -9,6 +9,7 @@ interface ExtensionListProps {
onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void> | void; onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void> | void;
onConfigure?: (extension: FixedExtensionEntry) => void; onConfigure?: (extension: FixedExtensionEntry) => void;
isStatic?: boolean; isStatic?: boolean;
disableConfiguration?: boolean;
} }
export default function ExtensionList({ export default function ExtensionList({
@@ -16,6 +17,7 @@ export default function ExtensionList({
onToggle, onToggle,
onConfigure, onConfigure,
isStatic, isStatic,
disableConfiguration: _disableConfiguration,
}: ExtensionListProps) { }: ExtensionListProps) {
return ( return (
<div className="grid grid-cols-2 gap-2 mb-2"> <div className="grid grid-cols-2 gap-2 mb-2">

View File

@@ -76,14 +76,14 @@ export function extensionToFormData(extension: FixedExtensionEntry): ExtensionFo
} }
return { return {
name: extension.name, name: extension.name || '',
description: description:
extension.type === 'stdio' || extension.type === 'sse' ? extension.description : undefined, extension.type === 'stdio' || extension.type === 'sse' ? extension.description || '' : '',
type: extension.type, type: extension.type === 'frontend' ? 'stdio' : extension.type,
cmd: extension.type === 'stdio' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined, cmd: extension.type === 'stdio' ? combineCmdAndArgs(extension.cmd, extension.args) : undefined,
endpoint: extension.type === 'sse' ? extension.uri : undefined, endpoint: extension.type === 'sse' ? extension.uri : undefined,
enabled: extension.enabled, enabled: extension.enabled,
timeout: 'timeout' in extension ? extension.timeout : undefined, timeout: 'timeout' in extension ? (extension.timeout ?? undefined) : undefined,
envVars, envVars,
}; };
} }
@@ -94,7 +94,7 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon
if (formData.type === 'stdio') { if (formData.type === 'stdio') {
// we put the cmd + args all in the form cmd field but need to split out into cmd + args // we put the cmd + args all in the form cmd field but need to split out into cmd + args
const { cmd, args } = splitCmdAndArgs(formData.cmd); const { cmd, args } = splitCmdAndArgs(formData.cmd || '');
return { return {
type: 'stdio', type: 'stdio',
@@ -111,7 +111,7 @@ export function createExtensionConfig(formData: ExtensionFormData): ExtensionCon
name: formData.name, name: formData.name,
description: formData.description, description: formData.description,
timeout: formData.timeout, timeout: formData.timeout,
uri: formData.endpoint, uri: formData.endpoint || '',
...(env_keys.length > 0 ? { env_keys } : {}), ...(env_keys.length > 0 ? { env_keys } : {}),
}; };
} else { } else {

View File

@@ -39,7 +39,7 @@ export function ConfigureApproveMode({
setIsSubmitting(true); setIsSubmitting(true);
try { try {
handleModeChange(approveMode); handleModeChange(approveMode || '');
onClose(); onClose();
} catch (error) { } catch (error) {
console.error('Error configuring goose mode:', error); console.error('Error configuring goose mode:', error);
@@ -68,8 +68,10 @@ export function ConfigureApproveMode({
key={mode.key} key={mode.key}
mode={mode} mode={mode}
showDescription={true} showDescription={true}
currentMode={approveMode} currentMode={approveMode || ''}
isApproveModeConfigure={true} isApproveModeConfigure={true}
parentView={'settings' as const}
setView={() => {}} // No-op since we're in configure mode
handleModeChange={(newMode) => { handleModeChange={(newMode) => {
setApproveMode(newMode); setApproveMode(newMode);
}} }}

View File

@@ -1,6 +1,6 @@
import { initializeAgent } from '../../../agent'; import { initializeAgent } from '../../../agent';
import { toastError, toastSuccess } from '../../../toasts'; import { toastError, toastSuccess } from '../../../toasts';
import { ProviderDetails } from '@/src/api'; import { ProviderDetails } from '../../../api';
import Model, { getProviderMetadata } from './modelInterface'; import Model, { getProviderMetadata } from './modelInterface';
import { ProviderMetadata } from '../../../api'; import { ProviderMetadata } from '../../../api';
@@ -37,7 +37,7 @@ export async function changeModel({ model, writeToConfig }: changeModelProps) {
toastError({ toastError({
title: CHANGE_MODEL_ERROR_TITLE, title: CHANGE_MODEL_ERROR_TITLE,
msg: SWITCH_MODEL_AGENT_ERROR_MSG, msg: SWITCH_MODEL_AGENT_ERROR_MSG,
traceback: error, traceback: error instanceof Error ? error.message : String(error),
}); });
// don't write to config // don't write to config
return; return;
@@ -51,7 +51,7 @@ export async function changeModel({ model, writeToConfig }: changeModelProps) {
toastError({ toastError({
title: CHANGE_MODEL_ERROR_TITLE, title: CHANGE_MODEL_ERROR_TITLE,
msg: CONFIG_UPDATE_ERROR_MSG, msg: CONFIG_UPDATE_ERROR_MSG,
traceback: error, traceback: error instanceof Error ? error.message : String(error),
}); });
// agent and config will be out of sync at this point // agent and config will be out of sync at this point
// TODO: reset agent to use current config settings // TODO: reset agent to use current config settings
@@ -92,7 +92,7 @@ export async function getCurrentModelAndProvider({
} }
export async function getFallbackModelAndProvider( export async function getFallbackModelAndProvider(
writeToConfig: (key: string, value: unknown, is_secret: boolean) => Promise<void> writeToConfig?: (key: string, value: unknown, is_secret: boolean) => Promise<void>
) { ) {
const provider = window.appConfig.get('GOOSE_DEFAULT_PROVIDER'); const provider = window.appConfig.get('GOOSE_DEFAULT_PROVIDER');
const model = window.appConfig.get('GOOSE_DEFAULT_MODEL'); const model = window.appConfig.get('GOOSE_DEFAULT_MODEL');
@@ -125,7 +125,7 @@ export async function getCurrentModelAndProviderForDisplay({
let metadata: ProviderMetadata; let metadata: ProviderMetadata;
try { try {
metadata = await getProviderMetadata(gooseProvider, getProviders); metadata = await getProviderMetadata(String(gooseProvider), getProviders);
} catch (error) { } catch (error) {
return { model: gooseModel, provider: gooseProvider }; return { model: gooseModel, provider: gooseProvider };
} }

View File

@@ -52,7 +52,7 @@ export function BaseModelsList({
); );
// no matches so just create a model object (maybe user updated config.yaml from CLI usage, manual editing etc) // no matches so just create a model object (maybe user updated config.yaml from CLI usage, manual editing etc)
if (!match) { if (!match) {
currentModel = { name: result.model, provider: result.provider }; currentModel = { name: String(result.model), provider: String(result.provider) };
} else { } else {
currentModel = match; currentModel = match;
} }
@@ -109,9 +109,9 @@ export function BaseModelsList({
writeToConfig: upsert, writeToConfig: upsert,
}); });
const currentModel = modelList.find( const currentModel =
(m) => m.name === result.model && m.provider === result.provider modelList.find((m) => m.name === result.model && m.provider === result.provider) ||
) || { name: result.model, provider: result.provider }; ({ name: String(result.model), provider: String(result.provider) } as Model);
setSelectedModel(currentModel); setSelectedModel(currentModel);
} catch (secondError) { } catch (secondError) {
@@ -136,10 +136,11 @@ export function BaseModelsList({
{modelList.map((model) => {modelList.map((model) =>
renderItem({ renderItem({
model, model,
isSelected: isSelected: !!(
selectedModel && selectedModel &&
selectedModel.name === model.name && selectedModel.name === model.name &&
selectedModel.provider === model.provider, selectedModel.provider === model.provider
),
onSelect: () => handleRadioChange(model), onSelect: () => handleRadioChange(model),
}) })
)} )}

View File

@@ -12,7 +12,17 @@ import type { View } from '../../../../App';
import Model, { getProviderMetadata } from '../modelInterface'; import Model, { getProviderMetadata } from '../modelInterface';
import { useModel } from '../../../settings/models/ModelContext'; import { useModel } from '../../../settings/models/ModelContext';
const ModalButtons = ({ onSubmit, onCancel, _isValid: _, _validationErrors: __ }) => ( const ModalButtons = ({
onSubmit,
onCancel,
_isValid: _,
_validationErrors: __,
}: {
onSubmit: () => void;
onCancel: () => void;
_isValid: boolean;
_validationErrors: { provider: string; model: string };
}) => (
<div> <div>
<Button <Button
type="submit" type="submit"
@@ -41,7 +51,9 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
const { getProviders, upsert } = useConfig(); const { getProviders, upsert } = useConfig();
const { switchModel } = useModel(); const { switchModel } = useModel();
const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]); const [providerOptions, setProviderOptions] = useState<{ value: string; label: string }[]>([]);
const [modelOptions, setModelOptions] = useState<{ options: { value: string; label: string; provider: string }[] }[]>([]); const [modelOptions, setModelOptions] = useState<
{ options: { value: string; label: string; provider: string }[] }[]
>([]);
const [provider, setProvider] = useState<string | null>(null); const [provider, setProvider] = useState<string | null>(null);
const [model, setModel] = useState<string>(''); const [model, setModel] = useState<string>('');
const [isCustomModel, setIsCustomModel] = useState(false); const [isCustomModel, setIsCustomModel] = useState(false);
@@ -80,7 +92,7 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
const isFormValid = validateForm(); const isFormValid = validateForm();
if (isFormValid) { if (isFormValid) {
const providerMetaData = await getProviderMetadata(provider, getProviders); const providerMetaData = await getProviderMetadata(provider || '', getProviders);
const providerDisplayName = providerMetaData.display_name; const providerDisplayName = providerMetaData.display_name;
const modelObj = { name: model, provider: provider, subtext: providerDisplayName } as Model; const modelObj = { name: model, provider: provider, subtext: providerDisplayName } as Model;
@@ -122,7 +134,9 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
]); ]);
// Format model options by provider // Format model options by provider
const formattedModelOptions = []; const formattedModelOptions: {
options: { value: string; label: string; provider: string }[];
}[] = [];
activeProviders.forEach(({ metadata, name }) => { activeProviders.forEach(({ metadata, name }) => {
if (metadata.known_models && metadata.known_models.length > 0) { if (metadata.known_models && metadata.known_models.length > 0) {
formattedModelOptions.push({ formattedModelOptions.push({
@@ -158,7 +172,8 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
: []; : [];
// Handle model selection change // Handle model selection change
const handleModelChange = (selectedOption) => { const handleModelChange = (newValue: unknown) => {
const selectedOption = newValue as { value: string; label: string; provider: string } | null;
if (selectedOption?.value === 'custom') { if (selectedOption?.value === 'custom') {
setIsCustomModel(true); setIsCustomModel(true);
setModel(''); setModel('');
@@ -169,7 +184,8 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
}; };
// Store the original model options in state, initialized from modelOptions // Store the original model options in state, initialized from modelOptions
const [originalModelOptions, setOriginalModelOptions] = useState<{ options: { value: string; label: string; provider: string }[] }[]>(modelOptions); const [originalModelOptions, setOriginalModelOptions] =
useState<{ options: { value: string; label: string; provider: string }[] }[]>(modelOptions);
const handleInputChange = (inputValue: string) => { const handleInputChange = (inputValue: string) => {
if (!provider) return; if (!provider) return;
@@ -221,8 +237,8 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
<ModalButtons <ModalButtons
onSubmit={onSubmit} onSubmit={onSubmit}
onCancel={onClose} onCancel={onClose}
isValid={isValid} _isValid={isValid}
validationErrors={validationErrors} _validationErrors={validationErrors}
/> />
} }
> >
@@ -252,7 +268,8 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
<Select <Select
options={providerOptions} options={providerOptions}
value={providerOptions.find((option) => option.value === provider) || null} value={providerOptions.find((option) => option.value === provider) || null}
onChange={(option) => { onChange={(newValue: unknown) => {
const option = newValue as { value: string; label: string } | null;
if (option?.value === 'configure_providers') { if (option?.value === 'configure_providers') {
// Navigate to ConfigureProviders view // Navigate to ConfigureProviders view
setView('ConfigureProviders'); setView('ConfigureProviders');

View File

@@ -102,7 +102,7 @@ export default memo(function ProviderGrid({
providers={providers} providers={providers}
isOnboarding={isOnboarding} isOnboarding={isOnboarding}
refreshProviders={refreshProviders} refreshProviders={refreshProviders}
onProviderLaunch={onProviderLaunch} onProviderLaunch={onProviderLaunch || (() => {})}
/> />
<ProviderConfigurationModal /> <ProviderConfigurationModal />
</ProviderModalProvider> </ProviderModalProvider>

View File

@@ -13,19 +13,23 @@ import { useConfig } from '../../../ConfigContext';
import { AlertTriangle } from 'lucide-react'; import { AlertTriangle } from 'lucide-react';
import { getCurrentModelAndProvider } from '../../models'; // Import the utility import { getCurrentModelAndProvider } from '../../models'; // Import the utility
const customSubmitHandlerMap = { interface FormValues {
[key: string]: string | number | boolean | null;
}
const customSubmitHandlerMap: Record<string, unknown> = {
provider_name: OllamaSubmitHandler, // example provider_name: OllamaSubmitHandler, // example
}; };
const customFormsMap = { const customFormsMap: Record<string, unknown> = {
provider_name: OllamaForm, // example provider_name: OllamaForm, // example
}; };
export default function ProviderConfigurationModal() { export default function ProviderConfigurationModal() {
const [validationErrors, setValidationErrors] = useState({}); const [validationErrors, setValidationErrors] = useState<Record<string, string>>({});
const { upsert, remove, read } = useConfig(); // Add read to the destructured values const { upsert, remove, read } = useConfig(); // Add read to the destructured values
const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal(); const { isOpen, currentProvider, modalProps, closeModal } = useProviderModal();
const [configValues, setConfigValues] = useState({}); const [configValues, setConfigValues] = useState<Record<string, string>>({});
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false); const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
const [isActiveProvider, setIsActiveProvider] = useState(false); // New state for tracking active provider const [isActiveProvider, setIsActiveProvider] = useState(false); // New state for tracking active provider
@@ -53,10 +57,14 @@ export default function ProviderConfigurationModal() {
: 'This will permanently delete the current provider configuration.' : 'This will permanently delete the current provider configuration.'
: `Add your API key(s) for this provider to integrate into Goose`; : `Add your API key(s) for this provider to integrate into Goose`;
const SubmitHandler = customSubmitHandlerMap[currentProvider.name] || DefaultSubmitHandler; const SubmitHandler =
const FormComponent = customFormsMap[currentProvider.name] || DefaultProviderSetupForm; (customSubmitHandlerMap[currentProvider.name] as typeof DefaultSubmitHandler) ||
DefaultSubmitHandler;
const FormComponent =
(customFormsMap[currentProvider.name] as typeof DefaultProviderSetupForm) ||
DefaultProviderSetupForm;
const handleSubmitForm = async (e) => { const handleSubmitForm = async (e: React.FormEvent) => {
e.preventDefault(); e.preventDefault();
console.log('Form submitted for:', currentProvider.name); console.log('Form submitted for:', currentProvider.name);
@@ -65,7 +73,7 @@ export default function ProviderConfigurationModal() {
// Validation logic // Validation logic
const parameters = currentProvider.metadata.config_keys || []; const parameters = currentProvider.metadata.config_keys || [];
const errors = {}; const errors: Record<string, string> = {};
// Check required fields // Check required fields
parameters.forEach((parameter) => { parameters.forEach((parameter) => {
@@ -94,7 +102,7 @@ export default function ProviderConfigurationModal() {
// Call onSubmit callback if provided (from modal props) // Call onSubmit callback if provided (from modal props)
if (modalProps.onSubmit) { if (modalProps.onSubmit) {
modalProps.onSubmit(configValues); modalProps.onSubmit(configValues as FormValues);
} }
} catch (error) { } catch (error) {
console.error('Failed to save configuration:', error); console.error('Failed to save configuration:', error);
@@ -156,7 +164,7 @@ export default function ProviderConfigurationModal() {
// Call onDelete callback if provided // Call onDelete callback if provided
// This should trigger the refreshProviders function // This should trigger the refreshProviders function
if (modalProps.onDelete) { if (modalProps.onDelete) {
modalProps.onDelete(currentProvider.name); modalProps.onDelete(currentProvider.name as unknown as FormValues);
} }
// Reset the delete confirmation state before closing // Reset the delete confirmation state before closing

View File

@@ -8,7 +8,7 @@ import OpenRouterLogo from './icons/openrouter@3x.png';
import DefaultLogo from './icons/default@3x.png'; import DefaultLogo from './icons/default@3x.png';
// Map provider names to their logos // Map provider names to their logos
const providerLogos = { const providerLogos: Record<string, string> = {
openai: OpenAILogo, openai: OpenAILogo,
anthropic: AnthropicLogo, anthropic: AnthropicLogo,
google: GoogleLogo, google: GoogleLogo,

View File

@@ -1,29 +1,7 @@
import React, { useEffect, useMemo, useState, useCallback } from 'react'; import React, { useEffect, useMemo, useState, useCallback } from 'react';
import { Input } from '../../../../../ui/input'; import { Input } from '../../../../../ui/input';
import { useConfig } from '../../../../../ConfigContext'; // Adjust this import path as needed import { useConfig } from '../../../../../ConfigContext'; // Adjust this import path as needed
import { ProviderDetails, ConfigKey } from '../../../../../../api';
interface ConfigParameter {
name: string;
required: boolean;
secret?: boolean;
default?: string | number | boolean | null;
}
interface ProviderMetadata {
config_keys?: ConfigParameter[];
display_name?: string;
description?: string;
known_models?: string[];
default_model?: string;
[key: string]: string | string[] | ConfigParameter[] | undefined;
}
interface Provider {
metadata: ProviderMetadata;
name: string;
is_configured: boolean;
[key: string]: string | boolean | ProviderMetadata;
}
interface ValidationErrors { interface ValidationErrors {
[key: string]: string; [key: string]: string;
@@ -32,7 +10,7 @@ interface ValidationErrors {
interface DefaultProviderSetupFormProps { interface DefaultProviderSetupFormProps {
configValues: Record<string, string>; configValues: Record<string, string>;
setConfigValues: React.Dispatch<React.SetStateAction<Record<string, string>>>; setConfigValues: React.Dispatch<React.SetStateAction<Record<string, string>>>;
provider: Provider; provider: ProviderDetails;
validationErrors: ValidationErrors; validationErrors: ValidationErrors;
} }
@@ -108,7 +86,7 @@ export default function DefaultProviderSetupForm({
}, [parameters]); }, [parameters]);
// Helper function to generate appropriate placeholder text // Helper function to generate appropriate placeholder text
const getPlaceholder = (parameter: ConfigParameter): string => { const getPlaceholder = (parameter: ConfigKey): string => {
// If default is defined and not null, show it // If default is defined and not null, show it
if (parameter.default !== undefined && parameter.default !== null) { if (parameter.default !== undefined && parameter.default !== null) {
return `Default: ${parameter.default}`; return `Default: ${parameter.default}`;

View File

@@ -1,18 +1,26 @@
import { PROVIDER_REGISTRY } from '../../../ProviderRegistry'; import { PROVIDER_REGISTRY } from '../../../ProviderRegistry';
import { Input } from '../../../../../ui/input'; import { Input } from '../../../../../ui/input';
import { useState, useEffect, useCallback } from 'react'; import React, { useState, useEffect, useCallback } from 'react';
import { RefreshCw } from 'lucide-react'; import { RefreshCw } from 'lucide-react';
import CustomRadio from '../../../../../ui/CustomRadio'; import CustomRadio from '../../../../../ui/CustomRadio';
export default function OllamaForm({ configValues, setConfigValues, provider }) { export default function OllamaForm({
configValues,
setConfigValues,
provider,
}: {
configValues: Record<string, string>;
setConfigValues: React.Dispatch<React.SetStateAction<Record<string, string>>>;
provider: { name: string; [key: string]: unknown };
}) {
const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name); const providerEntry = PROVIDER_REGISTRY.find((p) => p.name === provider.name);
const parameters = providerEntry?.details?.parameters || []; const parameters = providerEntry?.details?.parameters || [];
const [isCheckingLocal, setIsCheckingLocal] = useState(false); const [isCheckingLocal, setIsCheckingLocal] = useState(false);
const [isLocalAvailable, setIsLocalAvailable] = useState(false); const [isLocalAvailable, setIsLocalAvailable] = useState(false);
const handleConnectionTypeChange = useCallback( const handleConnectionTypeChange = useCallback(
(value) => { (value: string) => {
setConfigValues((prev) => ({ setConfigValues((prev) => ({
...prev, ...prev,
connection_type: value, connection_type: value,
@@ -22,7 +30,7 @@ export default function OllamaForm({ configValues, setConfigValues, provider })
); );
// Function to handle input changes and auto-select/deselect the host radio // Function to handle input changes and auto-select/deselect the host radio
const handleInputChange = (paramName, value) => { const handleInputChange = (paramName: string, value: string) => {
// Update the parameter value // Update the parameter value
setConfigValues((prev) => ({ setConfigValues((prev) => ({
...prev, ...prev,

View File

@@ -2,33 +2,50 @@
* Standalone function to submit provider configuration * Standalone function to submit provider configuration
* Useful for components that don't want to use the hook * Useful for components that don't want to use the hook
*/ */
export const DefaultSubmitHandler = async (upsertFn, provider, configValues) => { export const DefaultSubmitHandler = async (
upsertFn: (key: string, value: unknown, isSecret: boolean) => Promise<void>,
provider: {
metadata: {
config_keys?: Array<{
name: string;
required?: boolean;
default?: unknown;
secret?: boolean;
}>;
};
},
configValues: Record<string, unknown>
) => {
const parameters = provider.metadata.config_keys || []; const parameters = provider.metadata.config_keys || [];
const upsertPromises = parameters.map((parameter) => { const upsertPromises = parameters.map(
// Skip parameters that don't have a value and aren't required (parameter: { name: string; required?: boolean; default?: unknown; secret?: boolean }) => {
if (!configValues[parameter.name] && !parameter.required) { // Skip parameters that don't have a value and aren't required
return Promise.resolve(); if (!configValues[parameter.name] && !parameter.required) {
return Promise.resolve();
}
// For required parameters with no value, use the default if available
const value =
configValues[parameter.name] !== undefined
? configValues[parameter.name]
: parameter.default;
// Skip if there's still no value
if (value === undefined || value === null) {
return Promise.resolve();
}
// Create the provider-specific config key
const configKey = `${parameter.name}`;
// Explicitly define is_secret as a boolean (true/false)
const isSecret = parameter.secret === true;
// Pass the is_secret flag from the parameter definition
return upsertFn(configKey, value, isSecret);
} }
);
// For required parameters with no value, use the default if available
const value =
configValues[parameter.name] !== undefined ? configValues[parameter.name] : parameter.default;
// Skip if there's still no value
if (value === undefined || value === null) {
return Promise.resolve();
}
// Create the provider-specific config key
const configKey = `${parameter.name}`;
// Explicitly define is_secret as a boolean (true/false)
const isSecret = parameter.secret === true;
// Pass the is_secret flag from the parameter definition
return upsertFn(configKey, value, isSecret);
});
// Wait for all upsert operations to complete // Wait for all upsert operations to complete
return Promise.all(upsertPromises); return Promise.all(upsertPromises);

View File

@@ -1,4 +1,4 @@
export default function OllamaSubmitHandler(configValues) { export default function OllamaSubmitHandler(configValues: Record<string, unknown>) {
// Log each field value individually for clarity // Log each field value individually for clarity
console.log('Ollama field values:'); console.log('Ollama field values:');
Object.entries(configValues).forEach(([key, value]) => { Object.entries(configValues).forEach(([key, value]) => {

View File

@@ -45,7 +45,7 @@ export default function CardContainer({
}`} }`}
onClick={!grayedOut ? onClick : undefined} onClick={!grayedOut ? onClick : undefined}
style={{ style={{
cursor: !grayedOut && onClick ? 'pointer' : 'default', cursor: !grayedOut ? 'pointer' : 'default',
}} }}
> >
{!grayedOut && <GlowingRing />} {!grayedOut && <GlowingRing />}

View File

@@ -1,4 +1,3 @@
// Functions for string / string-based element creation (e.g. tooltips for each provider, descriptions, etc) // Functions for string / string-based element creation (e.g. tooltips for each provider, descriptions, etc)
export function OllamaNotConfiguredTooltipMessage() { export function OllamaNotConfiguredTooltipMessage() {
return ( return (

View File

@@ -10,12 +10,14 @@ export default function SessionSharingSection() {
// If env is set, force sharing enabled and set the baseUrl accordingly. // If env is set, force sharing enabled and set the baseUrl accordingly.
const [sessionSharingConfig, setSessionSharingConfig] = useState({ const [sessionSharingConfig, setSessionSharingConfig] = useState({
enabled: envBaseUrlShare ? true : false, enabled: envBaseUrlShare ? true : false,
baseUrl: envBaseUrlShare || '', baseUrl: typeof envBaseUrlShare === 'string' ? envBaseUrlShare : '',
}); });
const [urlError, setUrlError] = useState(''); const [urlError, setUrlError] = useState('');
// isUrlConfigured is true if the user has configured a baseUrl and it is valid. // isUrlConfigured is true if the user has configured a baseUrl and it is valid.
const isUrlConfigured = const isUrlConfigured =
!envBaseUrlShare && sessionSharingConfig.enabled && isValidUrl(sessionSharingConfig.baseUrl); !envBaseUrlShare &&
sessionSharingConfig.enabled &&
isValidUrl(String(sessionSharingConfig.baseUrl));
// Only load saved config from localStorage if the env variable is not provided. // Only load saved config from localStorage if the env variable is not provided.
useEffect(() => { useEffect(() => {
@@ -23,7 +25,7 @@ export default function SessionSharingSection() {
// If env variable is set, save the forced configuration to localStorage // If env variable is set, save the forced configuration to localStorage
const forcedConfig = { const forcedConfig = {
enabled: true, enabled: true,
baseUrl: envBaseUrlShare, baseUrl: typeof envBaseUrlShare === 'string' ? envBaseUrlShare : '',
}; };
localStorage.setItem('session_sharing_config', JSON.stringify(forcedConfig)); localStorage.setItem('session_sharing_config', JSON.stringify(forcedConfig));
} else { } else {
@@ -113,7 +115,7 @@ export default function SessionSharingSection() {
) : ( ) : (
<Switch <Switch
checked={sessionSharingConfig.enabled} checked={sessionSharingConfig.enabled}
disabled={envBaseUrlShare} disabled={!!envBaseUrlShare}
onCheckedChange={toggleSharing} onCheckedChange={toggleSharing}
variant="mono" variant="mono"
/> />
@@ -139,7 +141,7 @@ export default function SessionSharingSection() {
placeholder="https://example.com/api" placeholder="https://example.com/api"
value={sessionSharingConfig.baseUrl} value={sessionSharingConfig.baseUrl}
disabled={!!envBaseUrlShare} disabled={!!envBaseUrlShare}
onChange={envBaseUrlShare ? undefined : handleBaseUrlChange} {...(envBaseUrlShare ? {} : { onChange: handleBaseUrlChange })}
/> />
</div> </div>
{urlError && <p className="text-red-500 text-sm">{urlError}</p>} {urlError && <p className="text-red-500 text-sm">{urlError}</p>}

View File

@@ -1,4 +1,3 @@
export default function Box({ size }: { size: number }) { export default function Box({ size }: { size: number }) {
return ( return (
<svg <svg

View File

@@ -23,7 +23,6 @@ export function ConfirmationModal({
<BaseModal <BaseModal
isOpen={isOpen} isOpen={isOpen}
title={title} title={title}
onClose={onCancel}
actions={ actions={
<> <>
<button <button

View File

@@ -1,4 +1,3 @@
/** /**
* CustomRadio - A reusable radio button component with dark mode support * CustomRadio - A reusable radio button component with dark mode support
* @param {Object} props - Component props * @param {Object} props - Component props
@@ -25,6 +24,17 @@ const CustomRadio = ({
secondaryLabel = null, secondaryLabel = null,
rightContent = null, rightContent = null,
className = '', className = '',
}: {
id: string;
name: string;
value: string;
checked: boolean;
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
disabled?: boolean;
label?: React.ReactNode;
secondaryLabel?: React.ReactNode;
rightContent?: React.ReactNode;
className?: string;
}) => { }) => {
return ( return (
<label <label

View File

@@ -136,6 +136,9 @@ export function DeepLinkModal({ recipeConfig: initialRecipeConfig, onClose }: De
onClick={() => { onClick={() => {
// Open the deep link with the current bot config // Open the deep link with the current bot config
const currentConfig = { const currentConfig = {
id: 'deeplink-recipe',
name: 'DeepLink Recipe',
description: 'Recipe from deep link',
...recipeConfig, ...recipeConfig,
instructions, instructions,
activities, activities,

View File

@@ -1,6 +1,7 @@
import React from 'react';
import ReactSelect from 'react-select'; import ReactSelect from 'react-select';
export const Select = (props) => { export const Select = (props: React.ComponentProps<typeof ReactSelect>) => {
return ( return (
<ReactSelect <ReactSelect
{...props} {...props}

View File

@@ -1,4 +1,3 @@
export default function Send({ size }: { size: number }) { export default function Send({ size }: { size: number }) {
return ( return (
<svg <svg
@@ -12,7 +11,7 @@ export default function Send({ size }: { size: number }) {
<path <path
d="M22 12.5L2 4.5L4 12.5L2 20.5L22 12.5ZM5.81 13.5H14.11L4.88 17.19L5.81 13.5ZM14.11 11.5H5.81L4.89 7.81L14.11 11.5Z" d="M22 12.5L2 4.5L4 12.5L2 20.5L22 12.5ZM5.81 13.5H14.11L4.88 17.19L5.81 13.5ZM14.11 11.5H5.81L4.89 7.81L14.11 11.5Z"
fill="#7A7EFB" fill="#7A7EFB"
dark:fill="#4A56E2" className="dark:fill-[#4A56E2]"
/> />
</svg> </svg>
); );

View File

@@ -1,4 +1,3 @@
interface StopProps { interface StopProps {
size?: number; size?: number;
} }

View File

@@ -1,4 +1,3 @@
export default function VertDots({ size }: { size: number }) { export default function VertDots({ size }: { size: number }) {
return ( return (
<svg <svg

View File

@@ -1,4 +1,3 @@
export default function X({ size }: { size: number }) { export default function X({ size }: { size: number }) {
return ( return (
<svg <svg
@@ -13,7 +12,7 @@ export default function X({ size }: { size: number }) {
clipRule="evenodd" clipRule="evenodd"
d="M5.97237 6.00001L3.82593 3.85356L4.53303 3.14645L6.67948 5.2929L8.82593 3.14645L9.53303 3.85356L7.38659 6.00001L9.53303 8.14645L8.82593 8.85356L6.67948 6.70711L4.53303 8.85356L3.82593 8.14645L5.97237 6.00001Z" d="M5.97237 6.00001L3.82593 3.85356L4.53303 3.14645L6.67948 5.2929L8.82593 3.14645L9.53303 3.85356L7.38659 6.00001L9.53303 8.14645L8.82593 8.85356L6.67948 6.70711L4.53303 8.85356L3.82593 8.14645L5.97237 6.00001Z"
fill="black" fill="black"
dark:fill="white" className="dark:fill-white"
fillOpacity="0.6" fillOpacity="0.6"
/> />
</svg> </svg>

View File

@@ -1,4 +1,3 @@
export const BotIcon = () => { export const BotIcon = () => {
return ( return (
<svg <svg

View File

@@ -1,10 +1,13 @@
// Helper to construct API endpoints // Helper to construct API endpoints
export const getApiUrl = (endpoint: string): string => { export const getApiUrl = (endpoint: string): string => {
const baseUrl = window.appConfig.get('GOOSE_API_HOST') + ':' + window.appConfig.get('GOOSE_PORT'); const baseUrl =
String(window.appConfig.get('GOOSE_API_HOST') || '') +
':' +
String(window.appConfig.get('GOOSE_PORT') || '');
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`; const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
return `${baseUrl}${cleanEndpoint}`; return `${baseUrl}${cleanEndpoint}`;
}; };
export const getSecretKey = (): string => { export const getSecretKey = (): string => {
return window.appConfig.get('secretKey'); return String(window.appConfig.get('secretKey') || '');
}; };

Some files were not shown because too many files have changed in this diff Show More