Fix ESLint warnings and and enable max warnings 0 to fail builds (#2101)

This commit is contained in:
Zane
2025-04-09 15:13:53 -07:00
committed by GitHub
parent 0daff53110
commit 8451fb1c89
69 changed files with 968 additions and 685 deletions
-2
View File
@@ -1,6 +1,5 @@
import React, { useState, useEffect, useRef } from 'react';
import { useModel } from './settings/models/ModelContext';
import { useRecentModels } from './settings/models/RecentModels'; // Hook for recent models
import { Sliders } from 'lucide-react';
import { ModelRadioList } from './settings/models/ModelRadioList';
import { Document, ChevronUp, ChevronDown } from './icons';
@@ -19,7 +18,6 @@ export default function BottomMenu({
}) {
const [isModelMenuOpen, setIsModelMenuOpen] = useState(false);
const { currentModel } = useModel();
const { recentModels } = useRecentModels(); // Get recent models
const dropdownRef = useRef<HTMLDivElement>(null);
// Add effect to handle clicks outside
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { getApiUrl, getSecretKey } from '../config';
import { ChevronDown, ChevronUp } from './icons';
import {
@@ -16,35 +16,35 @@ export const BottomMenuModeSelection = () => {
const gooseModeDropdownRef = useRef<HTMLDivElement>(null);
const { read, upsert } = useConfig();
useEffect(() => {
const fetchCurrentMode = async () => {
try {
if (!settingsV2Enabled) {
const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': getSecretKey(),
},
});
const fetchCurrentMode = useCallback(async () => {
try {
if (!settingsV2Enabled) {
const response = await fetch(getApiUrl('/configs/get?key=GOOSE_MODE'), {
method: 'GET',
headers: {
'Content-Type': 'application/json',
'X-Secret-Key': getSecretKey(),
},
});
if (response.ok) {
const { value } = await response.json();
if (value) {
setGooseMode(value);
}
if (response.ok) {
const { value } = await response.json();
if (value) {
setGooseMode(value);
}
} else {
const mode = (await read('GOOSE_MODE', false)) as string;
setGooseMode(mode);
}
} catch (error) {
console.error('Error fetching current mode:', error);
} else {
const mode = (await read('GOOSE_MODE', false)) as string;
setGooseMode(mode);
}
};
} catch (error) {
console.error('Error fetching current mode:', error);
}
}, [read]);
useEffect(() => {
fetchCurrentMode();
}, []);
}, [fetchCurrentMode]);
useEffect(() => {
const handleEsc = (event: KeyboardEvent) => {
+30 -24
View File
@@ -4,7 +4,7 @@ import BottomMenu from './BottomMenu';
import FlappyGoose from './FlappyGoose';
import GooseMessage from './GooseMessage';
import Input from './Input';
import { type View } from '../App';
import { type View, ViewOptions } from '../App';
import LoadingGoose from './LoadingGoose';
import MoreMenuLayout from './more_menu/MoreMenuLayout';
import { Card } from './ui/card';
@@ -22,11 +22,9 @@ import {
ToolCall,
ToolCallResult,
ToolRequestMessageContent,
ToolResponse,
ToolResponseMessageContent,
ToolConfirmationRequestMessageContent,
getTextContent,
createAssistantMessage,
} from '../types/message';
export interface ChatType {
@@ -38,6 +36,26 @@ export interface ChatType {
messages: Message[];
}
interface GeneratedBotConfig {
id: string;
name: string;
description: string;
instructions: string;
activities: string[];
}
// Helper function to determine if a message is a user message
const isUserMessage = (message: Message): boolean => {
if (message.role === 'assistant') {
return false;
}
if (message.content.every((c) => c.type === 'toolConfirmationRequest')) {
return false;
}
return true;
};
export default function ChatView({
chat,
setChat,
@@ -46,16 +64,17 @@ export default function ChatView({
}: {
chat: ChatType;
setChat: (chat: ChatType) => void;
setView: (view: View, viewOptions?: Record<any, any>) => void;
setView: (view: View, viewOptions?: ViewOptions) => void;
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
}) {
const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
// Disabled askAi calls to save costs
// const [messageMetadata, setMessageMetadata] = useState<Record<string, string[]>>({});
const [hasMessages, setHasMessages] = useState(false);
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
const [showGame, setShowGame] = useState(false);
const [waitingForAgentResponse, setWaitingForAgentResponse] = useState(false);
const [showShareableBotModal, setshowShareableBotModal] = useState(false);
const [generatedBotConfig, setGeneratedBotConfig] = useState<any>(null);
const [generatedBotConfig, setGeneratedBotConfig] = useState<GeneratedBotConfig | null>(null);
const scrollRef = useRef<ScrollAreaHandle>(null);
// Get botConfig directly from appConfig
@@ -76,7 +95,7 @@ export default function ChatView({
api: getApiUrl('/reply'),
initialMessages: chat.messages,
body: { session_id: chat.id, session_working_dir: window.appConfig.get('GOOSE_WORKING_DIR') },
onFinish: async (message, _reason) => {
onFinish: async (_message, _reason) => {
window.electron.stopPowerSaveBlocker();
// Disabled askAi calls to save costs
@@ -94,7 +113,7 @@ export default function ChatView({
});
}
},
onToolCall: (toolCall) => {
onToolCall: (toolCall: string) => {
// Handle tool calls if needed
console.log('Tool call received:', toolCall);
// Implement tool call handling logic here
@@ -213,7 +232,7 @@ export default function ChatView({
const updatedChat = { ...prevChat, messages };
return updatedChat;
});
}, [messages]);
}, [messages, setChat]);
useEffect(() => {
if (messages.length > 0) {
@@ -304,8 +323,6 @@ export default function ChatView({
content: [],
};
// get the last tool's name or just "tool"
const lastToolName = toolRequests.at(-1)?.[1].value?.name ?? 'tool';
const notification = 'Interrupted by the user to make a correction';
// generate a response saying it was interrupted for each tool request
@@ -349,17 +366,6 @@ export default function ChatView({
return true;
});
const isUserMessage = (message: Message) => {
if (message.role === 'assistant') {
return false;
}
if (message.content.every((c) => c.type === 'toolConfirmationRequest')) {
return false;
}
return true;
};
const commandHistory = useMemo(() => {
return filteredMessages
.reduce<string[]>((history, message) => {
@@ -372,7 +378,7 @@ export default function ChatView({
return history;
}, [])
.reverse();
}, [filteredMessages, isUserMessage]);
}, [filteredMessages]);
return (
<div className="flex flex-col w-full h-screen items-center justify-center">
@@ -398,7 +404,7 @@ export default function ChatView({
messageHistoryIndex={chat?.messageHistoryIndex}
message={message}
messages={messages}
metadata={messageMetadata[message.id || '']}
// metadata={messageMetadata[message.id || '']}
append={(text) => append(createUserMessage(text))}
appendMessage={(newMessage) => {
const updatedMessages = [...messages, newMessage];
+110 -95
View File
@@ -1,4 +1,4 @@
import React, { createContext, useContext, useState, useEffect, useMemo } from 'react';
import React, { createContext, useContext, useState, useEffect, useMemo, useCallback } from 'react';
import {
readAllConfig,
readConfig,
@@ -70,6 +70,115 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
const [providersList, setProvidersList] = useState<ProviderDetails[]>([]);
const [extensionsList, setExtensionsList] = useState<FixedExtensionEntry[]>([]);
const reloadConfig = useCallback(async () => {
const response = await readAllConfig();
setConfig(response.data.config || {});
}, []);
const upsert = useCallback(
async (key: string, value: unknown, isSecret: boolean = false) => {
const query: UpsertConfigQuery = {
key: key,
value: value,
is_secret: isSecret,
};
await upsertConfig({
body: query,
});
await reloadConfig();
},
[reloadConfig]
);
const read = useCallback(async (key: string, is_secret: boolean = false) => {
const query: ConfigKeyQuery = { key: key, is_secret: is_secret };
const response = await readConfig({
body: query,
});
return response.data;
}, []);
const remove = useCallback(
async (key: string, is_secret: boolean) => {
const query: ConfigKeyQuery = { key: key, is_secret: is_secret };
await removeConfig({
body: query,
});
await reloadConfig();
},
[reloadConfig]
);
const addExtension = useCallback(
async (name: string, config: ExtensionConfig, enabled: boolean) => {
// remove shims if present
if (config.type === 'stdio') {
config.cmd = removeShims(config.cmd);
}
const query: ExtensionQuery = { name, config, enabled };
await apiAddExtension({
body: query,
});
await reloadConfig();
},
[reloadConfig]
);
const removeExtension = useCallback(
async (name: string) => {
await apiRemoveExtension({ path: { name: name } });
await reloadConfig();
},
[reloadConfig]
);
const getExtensions = useCallback(
async (forceRefresh = false): Promise<FixedExtensionEntry[]> => {
if (forceRefresh || extensionsList.length === 0) {
const result = await apiGetExtensions();
if (result.response.status === 422) {
throw new MalformedConfigError();
}
if (result.error && !result.data) {
console.log(result.error);
return extensionsList;
}
const extensionResponse: ExtensionResponse = result.data;
setExtensionsList(extensionResponse.extensions);
return extensionResponse.extensions;
}
return extensionsList;
},
[extensionsList]
);
const toggleExtension = useCallback(
async (name: string) => {
const exts = await getExtensions(true);
const extension = exts.find((ext) => ext.name === name);
if (extension) {
await addExtension(name, extension, !extension.enabled);
}
},
[addExtension, getExtensions]
);
const getProviders = useCallback(
async (forceRefresh = false): Promise<ProviderDetails[]> => {
if (forceRefresh || providersList.length === 0) {
const response = await providers();
setProvidersList(response.data);
return response.data;
}
return providersList;
},
[providersList]
);
useEffect(() => {
// Load all configuration data and providers on mount
(async () => {
@@ -95,100 +204,6 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
})();
}, []);
const reloadConfig = async () => {
const response = await readAllConfig();
setConfig(response.data.config || {});
};
const upsert = async (key: string, value: unknown, isSecret: boolean = false) => {
const query: UpsertConfigQuery = {
key: key,
value: value,
is_secret: isSecret,
};
await upsertConfig({
body: query,
});
await reloadConfig();
};
const read = async (key: string, is_secret: boolean = false) => {
const query: ConfigKeyQuery = { key: key, is_secret: is_secret };
const response = await readConfig({
body: query,
});
return response.data;
};
const remove = async (key: string, is_secret: boolean) => {
const query: ConfigKeyQuery = { key: key, is_secret: is_secret };
await removeConfig({
body: query,
});
await reloadConfig();
};
const addExtension = async (name: string, config: ExtensionConfig, enabled: boolean) => {
// remove shims if present
if (config.type == 'stdio') {
config.cmd = removeShims(config.cmd);
}
const query: ExtensionQuery = { name, config, enabled };
await apiAddExtension({
body: query,
});
await reloadConfig();
};
const removeExtension = async (name: string) => {
await apiRemoveExtension({ path: { name: name } });
await reloadConfig();
};
const toggleExtension = async (name: string) => {
// Get current extensions to find the one we need to toggle
const exts = await getExtensions(true);
const extension = exts.find((ext) => ext.name === name);
if (extension) {
// Toggle the enabled state and update using addExtension
await addExtension(name, extension, !extension.enabled);
}
};
const getProviders = async (forceRefresh = false): Promise<ProviderDetails[]> => {
if (forceRefresh || providersList.length === 0) {
// If a refresh is forced or we don't have providers yet
const response = await providers();
setProvidersList(response.data);
return response.data;
}
// Otherwise return the cached providers
return providersList;
};
const getExtensions = async (forceRefresh = false): Promise<FixedExtensionEntry[]> => {
// If a refresh is forced, or we don't have providers yet
if (forceRefresh || extensionsList.length === 0) {
const result = await apiGetExtensions();
if (result.response.status === 422) {
throw new MalformedConfigError();
}
if (result.error && !result.data) {
console.log(result.error);
return;
}
const extensionResponse: ExtensionResponse = result.data;
setExtensionsList(extensionResponse.extensions);
return extensionResponse.extensions;
}
// Otherwise return the cached providers
return extensionsList;
};
const contextValue = useMemo(
() => ({
config,
+64 -52
View File
@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useCallback } from 'react';
declare var requestAnimationFrame: (callback: FrameRequestCallback) => number;
declare class HTMLCanvasElement {}
@@ -51,57 +51,18 @@ const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => {
const OBSTACLE_WIDTH = 40;
const FLAP_DURATION = 150;
const safeRequestAnimationFrame = (callback: FrameRequestCallback) => {
const safeRequestAnimationFrame = useCallback((callback: FrameRequestCallback) => {
if (typeof window !== 'undefined' && typeof requestAnimationFrame !== 'undefined') {
requestAnimationFrame(callback);
}
};
// Load goose images
useEffect(() => {
const frames = [svg1, svg7];
frames.forEach((src, index) => {
const img = new Image();
img.src = src;
img.onload = () => {
framesLoaded.current += 1;
if (framesLoaded.current === frames.length) {
setImagesReady(true);
}
};
gooseImages.current[index] = img;
});
}, []);
const startGame = () => {
if (gameState.current.running || !imagesReady || typeof window === 'undefined') return;
const handleGameOver = useCallback(() => {
gameState.current.running = false;
setGameOver(true);
}, []);
gameState.current = {
gooseY: CANVAS_HEIGHT / 3,
velocity: 0,
obstacles: [],
gameLoop: 0,
running: true,
score: 0,
isFlapping: false,
flapEndTime: 0,
};
setGameOver(false);
setDisplayScore(0);
safeRequestAnimationFrame(gameLoop);
};
const flap = () => {
if (gameOver) {
startGame();
return;
}
gameState.current.velocity = FLAP_FORCE;
gameState.current.isFlapping = true;
gameState.current.flapEndTime = Date.now() + FLAP_DURATION;
};
const gameLoop = () => {
const gameLoop = useCallback(() => {
if (!gameState.current.running || !imagesReady) return;
const canvas = canvasRef.current;
if (!canvas) return;
@@ -209,12 +170,63 @@ const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => {
gameState.current.gameLoop++;
safeRequestAnimationFrame(gameLoop);
};
}, [
CANVAS_HEIGHT,
CANVAS_WIDTH,
GOOSE_SIZE,
GOOSE_X,
GRAVITY,
OBSTACLE_GAP,
OBSTACLE_SPEED,
OBSTACLE_WIDTH,
handleGameOver,
imagesReady,
safeRequestAnimationFrame,
]);
const handleGameOver = () => {
gameState.current.running = false;
setGameOver(true);
};
const startGame = useCallback(() => {
if (gameState.current.running || !imagesReady || typeof window === 'undefined') return;
gameState.current = {
gooseY: CANVAS_HEIGHT / 3,
velocity: 0,
obstacles: [],
gameLoop: 0,
running: true,
score: 0,
isFlapping: false,
flapEndTime: 0,
};
setGameOver(false);
setDisplayScore(0);
safeRequestAnimationFrame(gameLoop);
}, [CANVAS_HEIGHT, imagesReady, safeRequestAnimationFrame, gameLoop]);
const flap = useCallback(() => {
if (gameOver) {
startGame();
return;
}
gameState.current.velocity = FLAP_FORCE;
gameState.current.isFlapping = true;
gameState.current.flapEndTime = Date.now() + FLAP_DURATION;
}, [FLAP_DURATION, FLAP_FORCE, gameOver, startGame]);
// Load goose images
useEffect(() => {
const frames = [svg1, svg7];
frames.forEach((src, index) => {
const img = new Image();
img.src = src;
img.onload = () => {
framesLoaded.current += 1;
if (framesLoaded.current === frames.length) {
setImagesReady(true);
}
};
gooseImages.current[index] = img;
});
}, []);
useEffect(() => {
const canvas = canvasRef.current;
@@ -240,7 +252,7 @@ const FlappyGoose: React.FC<FlappyGooseProps> = ({ onClose }) => {
window.removeEventListener('keydown', handleKeyPress);
gameState.current.running = false;
};
}, [imagesReady]);
}, [CANVAS_HEIGHT, CANVAS_WIDTH, flap, imagesReady, startGame]);
return (
<div
+12 -2
View File
@@ -77,12 +77,22 @@ export default function GooseMessage({
useEffect(() => {
// If the message is the last message in the resumed session and has tool confirmation, it means the tool confirmation
// is broken or cancelled, to contonue use the session, we need to append a tool response to avoid mismatch tool result error.
if (messageIndex == messageHistoryIndex - 1 && hasToolConfirmation) {
if (
messageIndex === messageHistoryIndex - 1 &&
hasToolConfirmation &&
toolConfirmationContent
) {
appendMessage(
createToolErrorResponseMessage(toolConfirmationContent.id, 'The tool call is cancelled.')
);
}
}, []);
}, [
messageIndex,
messageHistoryIndex,
hasToolConfirmation,
toolConfirmationContent,
appendMessage,
]);
return (
<div className="goose-message flex w-[90%] justify-start opacity-0 animate-[appear_150ms_ease-in_forwards]">
+31 -34
View File
@@ -42,36 +42,33 @@ export default function Input({
}
}, []);
// Debounced function to update actual value
const debouncedSetValue = useCallback(
debounce((val: string) => {
setValue(val);
}, 150),
[]
);
// Debounced autosize function
const debouncedAutosize = useCallback(
debounce((textArea: HTMLTextAreaElement, value: string) => {
textArea.style.height = '0px'; // Reset height
const scrollHeight = textArea.scrollHeight;
textArea.style.height = Math.min(scrollHeight, maxHeight) + 'px';
}, 150),
[]
);
const useAutosizeTextArea = (textAreaRef: HTMLTextAreaElement | null, value: string) => {
useEffect(() => {
if (textAreaRef) {
debouncedAutosize(textAreaRef, value);
}
}, [textAreaRef, value]);
};
const minHeight = '1rem';
const maxHeight = 10 * 24;
useAutosizeTextArea(textAreaRef.current, displayValue);
// Debounced function to update actual value
const debouncedSetValue = useCallback((val: string) => {
debounce((value: string) => {
setValue(value);
}, 150)(val);
}, []);
// Debounced autosize function
const debouncedAutosize = useCallback(
(textArea: HTMLTextAreaElement) => {
debounce((element: HTMLTextAreaElement) => {
element.style.height = '0px'; // Reset height
const scrollHeight = element.scrollHeight;
element.style.height = Math.min(scrollHeight, maxHeight) + 'px';
}, 150)(textArea);
},
[maxHeight]
);
useEffect(() => {
if (textAreaRef.current) {
debouncedAutosize(textAreaRef.current);
}
}, [debouncedAutosize, displayValue]);
const handleChange = (evt: React.ChangeEvent<HTMLTextAreaElement>) => {
const val = evt.target.value;
@@ -82,17 +79,17 @@ export default function Input({
// Cleanup debounced functions on unmount
useEffect(() => {
return () => {
debouncedSetValue.cancel();
debouncedAutosize.cancel();
debouncedSetValue.cancel?.();
debouncedAutosize.cancel?.();
};
}, []);
}, [debouncedSetValue, debouncedAutosize]);
// Handlers for composition events, which are crucial for proper IME behavior
const handleCompositionStart = (evt: React.CompositionEvent<HTMLTextAreaElement>) => {
const handleCompositionStart = () => {
setIsComposing(true);
};
const handleCompositionEnd = (evt: React.CompositionEvent<HTMLTextAreaElement>) => {
const handleCompositionEnd = () => {
setIsComposing(false);
};
@@ -118,7 +115,7 @@ export default function Input({
}
}
if (newIndex == historyIndex) {
if (newIndex === historyIndex) {
return;
}
@@ -231,7 +228,7 @@ export default function Input({
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onStop();
onStop?.();
}}
className="absolute right-2 top-1/2 -translate-y-1/2 [&_svg]:size-5 text-textSubtle hover:text-textStandard"
>
@@ -4,15 +4,14 @@ import remarkGfm from 'remark-gfm';
import rehypeRaw from 'rehype-raw';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { oneDark } from 'react-syntax-highlighter/dist/esm/styles/prism';
import { oneLight } from 'react-syntax-highlighter/dist/cjs/styles/prism';
import { Check, Copy } from './icons';
import { visit } from 'unist-util-visit';
function rehypeinlineCodeProperty() {
return function (tree) {
if (!tree) return;
visit(tree, 'element', function (node, index, parent) {
if (node.tagName == 'code' && parent && parent.tagName === 'pre') {
visit(tree, 'element', function (node) {
if (node.tagName == 'code' && node.parent && node.parent.tagName === 'pre') {
node.properties.inlinecode = 'false';
} else {
node.properties.inlinecode = 'true';
@@ -75,8 +74,6 @@ const CodeBlock = ({ language, children }: { language: string; children: string
};
export default function MarkdownContent({ content, className = '' }: MarkdownContentProps) {
// Determine whether dark mode is enabled
const isDarkMode = document.documentElement.classList.contains('dark');
return (
<div className="w-full overflow-x-hidden">
<ReactMarkdown
@@ -100,8 +97,8 @@ export default function MarkdownContent({ content, className = '' }: MarkdownCon
${className}`}
components={{
a: ({ node, ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
code({ node, className, children, inlinecode, ...props }) {
a: ({ ...props }) => <a {...props} target="_blank" rel="noopener noreferrer" />,
code({ className, children, inlinecode, ...props }) {
const match = /language-(\w+)/.exec(className || 'language-text');
return inlinecode == 'false' && match ? (
<CodeBlock language={match[1]}>{String(children).replace(/\n$/, '')}</CodeBlock>
@@ -2,8 +2,16 @@ import { ChevronUp } from 'lucide-react';
import React, { useState } from 'react';
import MarkdownContent from './MarkdownContent';
type ToolCallArgumentValue =
| string
| number
| boolean
| null
| ToolCallArgumentValue[]
| { [key: string]: ToolCallArgumentValue };
interface ToolCallArgumentsProps {
args: Record<string, any>;
args: Record<string, ToolCallArgumentValue>;
}
export function ToolCallArguments({ args }: ToolCallArgumentsProps) {
@@ -13,7 +21,7 @@ export function ToolCallArguments({ args }: ToolCallArgumentsProps) {
setExpandedKeys((prev) => ({ ...prev, [key]: !prev[key] }));
};
const renderValue = (key: string, value: any) => {
const renderValue = (key: string, value: ToolCallArgumentValue) => {
if (typeof value === 'string') {
const needsExpansion = value.length > 60;
const isExpanded = expandedKeys[key];
@@ -45,18 +53,12 @@ export function ToolCallArguments({ args }: ToolCallArgumentsProps) {
onClick={() => toggleKey(key)}
className="text-sm hover:opacity-75 text-textStandard"
>
{/* {isExpanded ? '▼ ' : '▶ '} */}
<ChevronUp
className={`h-5 w-5 transition-all origin-center ${!isExpanded ? 'rotate-180' : ''}`}
/>
</button>
</div>
</div>
{/* {isExpanded && (
<div className="mt-2">
<MarkdownContent content={value} />
</div>
)} */}
</div>
);
}
@@ -44,21 +44,25 @@ export const SearchBar: React.FC<SearchBarProps> = ({
// Create debounced search function
const debouncedSearch = useCallback(
debounce((term: string, caseSensitive: boolean) => {
onSearch(term, caseSensitive);
}, 150),
[]
(term: string, isCaseSensitive: boolean) => {
debounce((searchTerm: string, caseSensitive: boolean) => {
onSearch(searchTerm, caseSensitive);
}, 150)(term, isCaseSensitive);
},
[onSearch]
);
useEffect(() => {
inputRef.current?.focus();
// Cleanup debounced function
return () => {
debouncedSearch.cancel();
};
}, []);
// Cleanup debounced function on unmount
useEffect(() => {
return () => {
debouncedSearch.cancel?.();
};
}, [debouncedSearch]);
const handleSearch = (event: React.ChangeEvent<HTMLInputElement>) => {
const value = event.target.value;
setDisplayTerm(value); // Update display immediately
@@ -93,7 +97,7 @@ export const SearchBar: React.FC<SearchBarProps> = ({
const handleClose = () => {
setIsExiting(true);
debouncedSearch.cancel(); // Cancel any pending searches
debouncedSearch.cancel?.(); // Cancel any pending searches
setTimeout(() => {
onClose();
}, 150); // Match animation duration
@@ -36,20 +36,25 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
// Create debounced highlight function
const debouncedHighlight = useCallback(
debounce((term: string, caseSensitive: boolean, highlighter: SearchHighlighter) => {
const highlights = highlighter.highlight(term, caseSensitive);
const count = highlights.length;
(term: string, caseSensitive: boolean, highlighter: SearchHighlighter) => {
debounce(
(searchTerm: string, isCaseSensitive: boolean, searchHighlighter: SearchHighlighter) => {
const highlights = searchHighlighter.highlight(searchTerm, isCaseSensitive);
const count = highlights.length;
if (count > 0) {
setSearchResults({
currentIndex: 1,
count,
});
highlighter.setCurrentMatch(0, true); // Explicitly scroll when setting initial match
} else {
setSearchResults(null);
}
}, 150),
if (count > 0) {
setSearchResults({
currentIndex: 1,
count,
});
searchHighlighter.setCurrentMatch(0, true); // Explicitly scroll when setting initial match
} else {
setSearchResults(null);
}
},
150
)(term, caseSensitive, highlighter);
},
[]
);
@@ -60,9 +65,9 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
highlighterRef.current.destroy();
highlighterRef.current = null;
}
debouncedHighlight.cancel();
debouncedHighlight.cancel?.();
};
}, []);
}, [debouncedHighlight]);
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
@@ -162,7 +167,7 @@ export const SearchView: React.FC<PropsWithChildren<SearchViewProps>> = ({
highlighterRef.current.clearHighlights();
}
// Cancel any pending highlight operations
debouncedHighlight.cancel();
debouncedHighlight.cancel?.();
};
return (
@@ -1,20 +1,10 @@
import {
Popover,
PopoverContent,
PopoverPortal,
PopoverTrigger,
} from '../../components/ui/popover';
import { Popover, PopoverContent, PopoverPortal, PopoverTrigger } from '../ui/popover';
import React, { useEffect, useState } from 'react';
import { ChatSmart, Idea, More, Refresh, Time, Send } from '../icons';
import { FolderOpen, Moon, Sliders, Sun } from 'lucide-react';
import { View } from '../../App';
import { useConfig } from '../ConfigContext';
import { settingsV2Enabled } from '../../flags';
interface VersionInfo {
current_version: string;
available_versions: string[];
}
import { ViewOptions, View } from '../../App';
interface MenuButtonProps {
onClick: () => void;
@@ -105,13 +95,11 @@ export default function MoreMenu({
setView,
setIsGoosehintsModalOpen,
}: {
setView: (view: View) => void;
setView: (view: View, viewOptions?: ViewOptions) => void;
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
}) {
const [open, setOpen] = useState(false);
const { remove } = useConfig();
const [versions, setVersions] = useState<VersionInfo | null>(null);
const [showVersions, setShowVersions] = useState(false);
const [themeMode, setThemeMode] = useState<'light' | 'dark' | 'system'>(() => {
const savedUseSystemTheme = localStorage.getItem('use_system_theme') === 'true';
if (savedUseSystemTheme) {
@@ -129,27 +117,6 @@ export default function MoreMenu({
return themeMode === 'dark';
});
useEffect(() => {
// Fetch available versions when the menu opens
const fetchVersions = async () => {
try {
const port = window.appConfig.get('GOOSE_PORT');
const response = await fetch(`http://127.0.0.1:${port}/agent/versions`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
setVersions(data);
} catch (error) {
console.error('Failed to fetch versions:', error);
}
};
if (open) {
fetchVersions();
}
}, [open]);
useEffect(() => {
const mediaQuery = window.matchMedia('(prefers-color-scheme: dark)');
@@ -196,6 +163,8 @@ export default function MoreMenu({
<PopoverTrigger asChild>
<button
className={`z-[100] absolute top-2 right-4 w-[20px] h-[20px] transition-colors cursor-pointer no-drag hover:text-textProminent ${open ? 'text-textProminent' : 'text-textSubtle'}`}
role="button"
aria-label="More options"
>
<More />
</button>
@@ -1,12 +1,12 @@
import MoreMenu from './MoreMenu';
import React from 'react';
import type { View } from '../../App';
import { View, ViewOptions } from '../../App';
export default function MoreMenuLayout({
setView,
setIsGoosehintsModalOpen,
}: {
setView: (view: View, viewOptions?: Record<any, any>) => void;
setView: (view: View, viewOptions?: ViewOptions) => void;
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
}) {
return (
@@ -39,7 +39,6 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
const [isSharing, setIsSharing] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const [canShare, setCanShare] = useState(false);
const [shareError, setShareError] = useState<string | null>(null);
useEffect(() => {
const savedSessionConfig = localStorage.getItem('session_sharing_config');
@@ -58,7 +57,6 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
const handleShare = async () => {
setIsSharing(true);
setShareError(null);
try {
// Get the session sharing configuration from localStorage
@@ -87,7 +85,6 @@ const SessionHistoryView: React.FC<SessionHistoryViewProps> = ({
setIsShareModalOpen(true);
} catch (error) {
console.error('Error sharing session:', error);
setShareError(error instanceof Error ? error.message : 'Unknown error occurred');
toast.error(
`Failed to share session: ${error instanceof Error ? error.message : 'Unknown error'}`
);
@@ -1,5 +1,4 @@
import React, { useEffect, useState } from 'react';
import { ViewConfig } from '../../App';
import {
MessageSquareText,
Target,
@@ -14,9 +13,10 @@ import { Card } from '../ui/card';
import { Button } from '../ui/button';
import BackButton from '../ui/BackButton';
import { ScrollArea } from '../ui/scroll-area';
import { View, ViewOptions } from '../../App';
interface SessionListViewProps {
setView: (view: ViewConfig['view'], viewOptions?: Record<any, any>) => void;
setView: (view: View, viewOptions?: ViewOptions) => void;
onSelectSession: (sessionId: string) => void;
}
@@ -1,12 +1,12 @@
import React, { useState } from 'react';
import { ViewConfig } from '../../App';
import { View, ViewOptions } from '../../App';
import { fetchSessionDetails, type SessionDetails } from '../../sessions';
import SessionListView from './SessionListView';
import SessionHistoryView from './SessionHistoryView';
import { toastError } from '../../toasts';
interface SessionsViewProps {
setView: (view: ViewConfig['view'], viewOptions?: Record<any, any>) => void;
setView: (view: View, viewOptions?: ViewOptions) => void;
}
const SessionsView: React.FC<SessionsViewProps> = ({ setView }) => {
@@ -23,8 +23,10 @@ interface OllamaBattleGameProps {
}
export function OllamaBattleGame({ onComplete, _requiredKeys }: OllamaBattleGameProps) {
// Use type assertion for audioRef to avoid DOM lib dependency
const audioRef = useRef<any>(null);
// Use Audio element type for audioRef
const audioRef = useRef<{ play: () => Promise<void>; pause: () => void; volume: number } | null>(
null
);
const [isMuted, setIsMuted] = useState(false);
const [battleState, setBattleState] = useState<BattleState>({
@@ -1,4 +1,5 @@
import React, { useState, useEffect } from 'react';
import { IpcRendererEvent } from 'electron';
import { ScrollArea } from '../ui/scroll-area';
import { Settings as SettingsType } from './types';
import {
@@ -13,7 +14,7 @@ import { ConfigureBuiltInExtensionModal } from './extensions/ConfigureBuiltInExt
import BackButton from '../ui/BackButton';
import { RecentModelsRadio } from './models/RecentModels';
import { ExtensionItem } from './extensions/ExtensionItem';
import type { View } from '../../App';
import { View, ViewOptions } from '../../App';
import { ModeSelection } from './basic/ModeSelection';
import SessionSharingSection from './session/SessionSharingSection';
import { toastSuccess } from '../../toasts';
@@ -59,7 +60,7 @@ export default function SettingsView({
viewOptions,
}: {
onClose: () => void;
setView: (view: View) => void;
setView: (view: View, viewOptions?: ViewOptions) => void;
viewOptions: SettingsViewOptions;
}) {
const [settings, setSettings] = React.useState<SettingsType>(() => {
@@ -92,7 +93,7 @@ export default function SettingsView({
// Listen for settings updates from extension storage
useEffect(() => {
const handleSettingsUpdate = (_: any) => {
const handleSettingsUpdate = (_event: IpcRendererEvent) => {
const saved = localStorage.getItem('user_settings');
if (saved) {
let currentSettings = JSON.parse(saved);
@@ -101,8 +101,18 @@ export async function getProvidersList(): Promise<Provider[]> {
const data = await response.json();
interface ProviderItem {
id: string;
details?: {
name?: string;
description?: string;
models?: string[];
required_keys?: string[];
};
}
// Format the response into an array of providers
return data.map((item: any) => ({
return data.map((item: ProviderItem) => ({
id: item.id, // Root-level ID
name: item.details?.name || 'Unknown Provider', // Nested name in details
description: item.details?.description || 'No description available.', // Nested description
@@ -4,7 +4,6 @@ import { Button } from '../../ui/button';
import { Input } from '../../ui/input';
import { FullExtensionConfig, DEFAULT_EXTENSION_TIMEOUT } from '../../../extensions';
import { Select } from '../../ui/Select';
import { createDarkSelectStyles, darkSelectTheme } from '../../ui/select-styles';
import { getApiUrl, getSecretKey } from '../../../config';
import { toastError } from '../../../toasts';
@@ -1,5 +1,3 @@
import { Model } from './ModelContext';
export const openai_models = ['gpt-4o-mini', 'gpt-4o', 'gpt-4-turbo', 'o1'];
export const anthropic_models = [
@@ -1,11 +1,9 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import { Check, Plus, Settings, X, Rocket } from 'lucide-react';
import { Button } from '../../ui/button';
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '../../ui/Tooltip';
import { Portal } from '@radix-ui/react-portal';
import { required_keys } from '../models/hardcoded_stuff';
import { useActiveKeys } from '../api_keys/ActiveKeysContext';
import { getActiveProviders } from '../api_keys/utils';
// Common interfaces and helper functions
interface Provider {
@@ -67,7 +65,6 @@ function BaseProviderCard({
}: BaseProviderCardProps) {
const numRequiredKeys = required_keys[name]?.length || 0;
const tooltipText = numRequiredKeys === 1 ? `Add ${name} API Key` : `Add ${name} API Keys`;
const { activeKeys, setActiveKeys } = useActiveKeys();
return (
<div className="relative h-full p-[2px] overflow-hidden rounded-[9px] group/card bg-borderSubtle hover:bg-transparent hover:duration-300">
@@ -4,7 +4,6 @@ import { BaseProviderGrid, getProviderDescription } from './BaseProviderGrid';
import { supported_providers, provider_aliases, required_keys } from '../models/hardcoded_stuff';
import { ProviderSetupModal } from '../ProviderSetupModal';
import { getApiUrl, getSecretKey } from '../../../config';
import { toast } from 'react-toastify';
import { getActiveProviders, isSecretKey } from '../api_keys/utils';
import { useModel } from '../models/ModelContext';
import { Button } from '../../ui/button';
@@ -15,11 +15,9 @@ export type SettingsViewOptions = {
export default function SettingsView({
onClose,
setView,
viewOptions,
}: {
onClose: () => void;
setView: (view: View) => void;
viewOptions: SettingsViewOptions;
}) {
return (
<div className="h-screen w-full animate-[fadein_200ms_ease-in_forwards]">
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import { Button } from '../../ui/button';
import { Plus } from 'lucide-react';
import { GPSIcon } from '../../ui/icons';
@@ -17,39 +17,29 @@ import { activateExtension, deleteExtension, toggleExtension, updateExtension }
export default function ExtensionsSection() {
const { getExtensions, addExtension, removeExtension } = useConfig();
const [loading, setLoading] = useState<boolean>(true);
const [error, setError] = useState<string | null>(null);
const [extensions, setExtensions] = useState<FixedExtensionEntry[]>([]);
const [selectedExtension, setSelectedExtension] = useState<FixedExtensionEntry | null>(null);
const [isModalOpen, setIsModalOpen] = useState(false);
const [isAddModalOpen, setIsAddModalOpen] = useState(false);
// We don't need errorFormData anymore since we're not reopening modals on failure
const fetchExtensions = async () => {
setLoading(true);
try {
const extensionsList = await getExtensions(true); // Force refresh
// Sort extensions by name to maintain consistent order
const sortedExtensions = [...extensionsList].sort((a, b) => a.name.localeCompare(b.name));
setExtensions(sortedExtensions);
setError(null);
} catch (err) {
setError('Failed to load extensions');
console.error('Error loading extensions:', err);
} finally {
setLoading(false);
}
};
const fetchExtensions = useCallback(async () => {
const extensionsList = await getExtensions(true); // Force refresh
// Sort extensions by name to maintain consistent order
const sortedExtensions = [...extensionsList].sort((a, b) => a.name.localeCompare(b.name));
setExtensions(sortedExtensions);
}, [getExtensions]);
useEffect(() => {
fetchExtensions();
}, []);
}, [fetchExtensions]);
const handleExtensionToggle = async (extension: FixedExtensionEntry) => {
// If extension is enabled, we are trying to toggle if off, otherwise on
const toggleDirection = extension.enabled ? 'toggleOff' : 'toggleOn';
const extensionConfig = extractExtensionConfig(extension);
// eslint-disable-next-line no-useless-catch
try {
await toggleExtension({
toggle: toggleDirection,
@@ -3,12 +3,17 @@ import { getApiUrl, getSecretKey } from '../../../config';
import { toastService, ToastServiceOptions } from '../../../toasts';
import { replaceWithShims } from './utils';
interface ApiResponse {
error?: boolean;
message?: string;
}
/**
* Makes an API call to the extension endpoints
*/
export async function extensionApiCall(
endpoint: string,
payload: any,
payload: ExtensionConfig | string,
options: ToastServiceOptions = {}
): Promise<Response> {
// Configure toast notifications
@@ -118,7 +123,7 @@ function handleErrorResponse(
}
// Safely parses JSON response
async function parseResponseData(response: Response): Promise<any> {
async function parseResponseData(response: Response): Promise<ApiResponse> {
try {
const text = await response.text();
return text ? JSON.parse(text) : { error: false };
@@ -7,6 +7,46 @@ interface ActivateExtensionProps {
extensionConfig: ExtensionConfig;
}
type ExtensionError = {
message?: string;
code?: number;
name?: string;
stack?: string;
};
type RetryOptions = {
retries?: number;
delayMs?: number;
shouldRetry?: (error: ExtensionError, attempt: number) => boolean;
backoffFactor?: number; // multiplier for exponential backoff
};
async function retryWithBackoff<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
const { retries = 3, delayMs = 1000, backoffFactor = 1.5, shouldRetry = () => true } = options;
let attempt = 0;
let lastError: ExtensionError;
while (attempt <= retries) {
try {
return await fn();
} catch (err) {
lastError = err as ExtensionError;
attempt++;
if (attempt > retries || !shouldRetry(lastError, attempt)) {
break;
}
const waitTime = delayMs * Math.pow(backoffFactor, attempt - 1);
console.warn(`Retry attempt ${attempt} failed. Retrying in ${waitTime}ms...`, err);
await new Promise((res) => setTimeout(res, waitTime));
}
}
throw lastError;
}
/**
* Activates an extension by adding it to both the config system and the API.
* @param props The extension activation properties
@@ -43,39 +83,6 @@ export async function activateExtension({
}
}
type RetryOptions = {
retries?: number;
delayMs?: number;
shouldRetry?: (error: unknown, attempt: number) => boolean;
backoffFactor?: number; // multiplier for exponential backoff
};
async function retryWithBackoff<T>(fn: () => Promise<T>, options: RetryOptions = {}): Promise<T> {
const { retries = 3, delayMs = 1000, backoffFactor = 1.5, shouldRetry = () => true } = options;
let attempt = 0;
let lastError: unknown;
while (attempt <= retries) {
try {
return await fn();
} catch (err) {
lastError = err;
attempt++;
if (attempt > retries || !shouldRetry(err, attempt)) {
break;
}
const waitTime = delayMs * Math.pow(backoffFactor, attempt - 1);
console.warn(`Retry attempt ${attempt} failed. Retrying in ${waitTime}ms...`, err);
await new Promise((res) => setTimeout(res, waitTime));
}
}
throw lastError;
}
interface AddToAgentOnStartupProps {
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
extensionConfig: ExtensionConfig;
@@ -92,7 +99,7 @@ export async function addToAgentOnStartup({
await retryWithBackoff(() => addToAgent(extensionConfig, { silent: true }), {
retries: 3,
delayMs: 1000,
shouldRetry: (error: any) =>
shouldRetry: (error: ExtensionError) =>
error.message &&
(error.message.includes('428') ||
error.message.includes('Precondition Required') ||
@@ -103,7 +110,7 @@ export async function addToAgentOnStartup({
toastService.error({
title: extensionConfig.name,
msg: 'Extension failed to start and will be disabled.',
traceback: finalError,
traceback: finalError as Error,
});
try {
@@ -5,7 +5,7 @@ interface ExtensionConfigFieldsProps {
type: 'stdio' | 'sse' | 'builtin';
full_cmd: string;
endpoint: string;
onChange: (key: string, value: any) => void;
onChange: (key: string, value: string) => void;
submitAttempted?: boolean;
isValid?: boolean;
}
@@ -1,12 +1,12 @@
import { Input } from '../../../ui/input';
import { Select } from '../../../ui/Select';
import React, { useState } from 'react';
import React from 'react';
interface ExtensionInfoFieldsProps {
name: string;
type: 'stdio' | 'sse' | 'builtin';
description: string;
onChange: (key: string, value: any) => void;
onChange: (key: string, value: string) => void;
submitAttempted: boolean;
}
@@ -1,10 +1,9 @@
import { Input } from '../../../ui/input';
import Select from 'react-select';
import React, { useState } from 'react';
import React from 'react';
interface ExtensionTimeoutFieldProps {
timeout: number;
onChange: (key: string, value: any) => void;
onChange: (key: string, value: string | number) => void;
submitAttempted: boolean;
}
@@ -1,6 +1,5 @@
// Default extension timeout in seconds
// TODO: keep in sync with rust better
import * as module from 'node:module';
export const DEFAULT_EXTENSION_TIMEOUT = 300;
@@ -132,7 +131,8 @@ export function combineCmdAndArgs(cmd: string, args: string[]): string {
* @returns The ExtensionConfig portion of the object
*/
export function extractExtensionConfig(fixedEntry: FixedExtensionEntry): ExtensionConfig {
const { enabled, ...extensionConfig } = fixedEntry;
// todo: enabled not used?
const { ...extensionConfig } = fixedEntry;
return extensionConfig;
}
@@ -1,10 +1,6 @@
import React, { useEffect, useState } from 'react';
import { getApiUrl, getSecretKey } from '../../../config';
import { all_goose_modes, filterGooseModes, ModeSelectionItem } from './ModeSelectionItem';
import ExtensionList from '@/src/components/settings_v2/extensions/subcomponents/ExtensionList';
import { Button } from '@/src/components/ui/button';
import { Plus } from 'lucide-react';
import { GPSIcon } from '@/src/components/ui/icons';
export const ModeSection = () => {
const [currentMode, setCurrentMode] = useState('auto');
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useEffect, useState, useCallback } from 'react';
import type { View } from '../../../App';
import ModelSettingsButtons from './subcomponents/ModelSettingsButtons';
import { useConfig } from '../../ConfigContext';
@@ -16,7 +16,7 @@ export default function ModelsSection({ setView }: ModelsSectionProps) {
const { read, getProviders } = useConfig();
// Function to load model data
const loadModelData = async () => {
const loadModelData = useCallback(async () => {
try {
const gooseModel = (await read('GOOSE_MODEL', false)) as string;
const gooseProvider = (await read('GOOSE_PROVIDER', false)) as string;
@@ -40,7 +40,7 @@ export default function ModelsSection({ setView }: ModelsSectionProps) {
} catch (error) {
console.error('Error loading model data:', error);
}
};
}, [read, getProviders]);
useEffect(() => {
// Initial load
@@ -55,7 +55,7 @@ export default function ModelsSection({ setView }: ModelsSectionProps) {
return () => {
clearInterval(interval);
};
}, []);
}, [loadModelData]);
return (
<section id="models" className="px-8">
@@ -4,10 +4,10 @@ import React, { useEffect, useState, useRef } from 'react';
import { useConfig } from '../../../ConfigContext';
import { getCurrentModelAndProviderForDisplay } from '../index';
import { AddModelModal } from '../subcomponents/AddModelModal';
import type { View } from '../../../../App';
import { View } from '../../../../App';
interface ModelsBottomBarProps {
dropdownRef: any;
dropdownRef: React.RefObject<HTMLDivElement>;
setView: (view: View) => void;
}
export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBarProps) {
@@ -72,7 +72,7 @@ export function BaseModelsList({
return () => {
isMounted = false;
};
}, [read]);
}, [read, modelList, upsert]);
const handleModelSelection = async (model: Model) => {
await changeModel({ model: model, writeToConfig: upsert, getExtensions, addExtension });
@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import Model from '../modelInterface';
const MAX_RECENT_MODELS = 3;
@@ -1,5 +1,5 @@
import React, { useEffect, useState } from 'react';
import { ArrowLeftRight, ExternalLink, Plus } from 'lucide-react';
import React, { useEffect, useState, useCallback } from 'react';
import { ArrowLeftRight, ExternalLink } from 'lucide-react';
import Modal from '../../../Modal';
import { Button } from '../../../ui/button';
@@ -11,7 +11,7 @@ import { changeModel } from '../index';
import type { View } from '../../../../App';
import Model, { getProviderMetadata } from '../modelInterface';
const ModalButtons = ({ onSubmit, onCancel, isValid, validationErrors }) => (
const ModalButtons = ({ onSubmit, onCancel, _isValid, _validationErrors }) => (
<div>
<Button
type="submit"
@@ -51,7 +51,7 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
const [attemptedSubmit, setAttemptedSubmit] = useState(false);
// Validate form data
const validateForm = () => {
const validateForm = useCallback(() => {
const errors = {
provider: '',
model: '',
@@ -71,7 +71,7 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
setValidationErrors(errors);
setIsValid(formIsValid);
return formIsValid;
};
}, [model, provider]);
const onSubmit = async () => {
setAttemptedSubmit(true);
@@ -96,7 +96,7 @@ export const AddModelModal = ({ onClose, setView }: AddModelModalProps) => {
if (attemptedSubmit) {
validateForm();
}
}, [provider, model, attemptedSubmit]);
}, [provider, model, attemptedSubmit, validateForm]);
useEffect(() => {
(async () => {
@@ -72,7 +72,13 @@ const ProviderCards = memo(function ProviderCards({
isOnboarding={isOnboarding}
/>
));
}, [providers, isOnboarding, configureProviderViaModal, onProviderLaunch]);
}, [
providers,
isOnboarding,
configureProviderViaModal,
deleteProviderConfigViaModal,
onProviderLaunch,
]);
return <>{providerCards}</>;
});
@@ -1,6 +1,4 @@
import ProviderDetails from './interfaces/ProviderDetails';
import OllamaForm from './modal/subcomponents/forms/OllamaForm';
import OllamaSubmitHandler from './modal/subcomponents/handlers/OllamaSubmitHandler';
export interface ProviderRegistry {
name: string;
@@ -7,7 +7,6 @@ import { ProviderDetails } from '../../../api/types.gen';
import { initializeSystem } from '../../../utils/providerUtils';
import WelcomeGooseLogo from '../../WelcomeGooseLogo';
import { toastService } from '../../../toasts';
import { toast } from 'react-toastify';
interface ProviderSettingsProps {
onClose: () => void;
@@ -84,7 +83,7 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett
});
onClose();
},
[onClose, upsert]
[onClose, upsert, getExtensions, addExtension]
);
return (
@@ -1,3 +1,4 @@
import React from 'react';
import ParameterSchema from '../interfaces/ParameterSchema';
import ProviderSetupFormProps from '../modal/interfaces/ProviderSetupFormProps';
@@ -8,5 +9,5 @@ export default interface ProviderDetails {
parameters: ParameterSchema[];
getTags?: (name: string) => string[];
customForm?: React.ComponentType<ProviderSetupFormProps>;
customSubmit?: (e: any) => void;
customSubmit?: (e: React.SyntheticEvent) => void;
}
@@ -1,7 +1,11 @@
interface ProviderMetadata {
[key: string]: string | number | boolean | null;
}
// runtime data per instance
export default interface ProviderState {
id: string;
name: string;
isConfigured: boolean;
metadata: any;
metadata: ProviderMetadata;
}
@@ -1,11 +1,19 @@
import React, { createContext, useContext, useState } from 'react';
import { ProviderDetails } from '../../../../api/types.gen';
interface FormValues {
[key: string]: string | number | boolean | null;
}
interface ModalProps {
onSubmit?: (values: any) => void;
onSubmit?: (values: FormValues) => void;
onCancel?: () => void;
onDelete?: (values: any) => void;
formProps?: any;
onDelete?: (values: FormValues) => void;
formProps?: {
initialValues?: FormValues;
validationSchema?: object;
[key: string]: unknown;
};
}
interface ProviderModalContextType {
@@ -1,10 +1,10 @@
import React from 'react';
import React, { SyntheticEvent } from 'react';
import { Button } from '../../../../ui/button';
import { Trash2, AlertTriangle } from 'lucide-react';
interface ProviderSetupActionsProps {
onCancel: () => void;
onSubmit: (e: any) => void;
onSubmit: (e: SyntheticEvent) => void;
onDelete?: () => void;
showDeleteConfirmation?: boolean;
onConfirmDelete?: () => void;
@@ -1,12 +1,39 @@
import React, { useEffect, useMemo, useState } from 'react';
import React, { useEffect, useMemo, useState, useCallback } from 'react';
import { Input } from '../../../../../ui/input';
import { useConfig } from '../../../../../ConfigContext'; // Adjust this import path as needed
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 {
[key: string]: string;
}
interface DefaultProviderSetupFormProps {
configValues: Record<string, any>;
setConfigValues: React.Dispatch<React.SetStateAction<Record<string, any>>>;
provider: any;
validationErrors: any;
configValues: Record<string, string>;
setConfigValues: React.Dispatch<React.SetStateAction<Record<string, string>>>;
provider: Provider;
validationErrors: ValidationErrors;
}
export default function DefaultProviderSetupForm({
@@ -15,61 +42,64 @@ export default function DefaultProviderSetupForm({
provider,
validationErrors = {},
}: DefaultProviderSetupFormProps) {
const parameters = provider.metadata.config_keys || [];
const parameters = useMemo(
() => provider.metadata.config_keys || [],
[provider.metadata.config_keys]
);
const [isLoading, setIsLoading] = useState(true);
const { read } = useConfig();
console.log('configValues default form', configValues);
// Initialize values when the component mounts or provider changes
useEffect(() => {
const loadConfigValues = async () => {
setIsLoading(true);
const newValues = { ...configValues };
const loadConfigValues = useCallback(async () => {
setIsLoading(true);
const newValues = { ...configValues };
// Try to load actual values from config for each parameter that is not secret
for (const parameter of parameters) {
if (parameter.required) {
try {
// Check if there's a stored value in the config system
const configKey = `${parameter.name}`;
const configResponse = await read(configKey, parameter.secret || false);
// Try to load actual values from config for each parameter that is not secret
for (const parameter of parameters) {
if (parameter.required) {
try {
// Check if there's a stored value in the config system
const configKey = `${parameter.name}`;
const configResponse = await read(configKey, parameter.secret || false);
if (configResponse) {
// Use the value from the config provider
newValues[parameter.name] = configResponse;
} else if (
parameter.default !== undefined &&
parameter.default !== null &&
!configValues[parameter.name]
) {
// Fall back to default value if no config value exists
newValues[parameter.name] = parameter.default;
}
} catch (error) {
console.error(`Failed to load config for ${parameter.name}:`, error);
// Fall back to default if read operation fails
if (
parameter.default !== undefined &&
parameter.default !== null &&
!configValues[parameter.name]
) {
newValues[parameter.name] = parameter.default;
}
if (configResponse) {
// Use the value from the config provider
newValues[parameter.name] = String(configResponse);
} else if (
parameter.default !== undefined &&
parameter.default !== null &&
!configValues[parameter.name]
) {
// Fall back to default value if no config value exists
newValues[parameter.name] = String(parameter.default);
}
} catch (error) {
console.error(`Failed to load config for ${parameter.name}:`, error);
// Fall back to default if read operation fails
if (
parameter.default !== undefined &&
parameter.default !== null &&
!configValues[parameter.name]
) {
newValues[parameter.name] = String(parameter.default);
}
}
}
}
// Update state with loaded values
setConfigValues((prev) => ({
...prev,
...newValues,
}));
setIsLoading(false);
};
// Update state with loaded values
setConfigValues((prev) => ({
...prev,
...newValues,
}));
setIsLoading(false);
}, [configValues, parameters, read, setConfigValues]);
loadConfigValues().then();
}, []);
useEffect(() => {
loadConfigValues();
}, [loadConfigValues]);
// Filter parameters to only show required ones
const requiredParameters = useMemo(() => {
@@ -77,7 +107,7 @@ export default function DefaultProviderSetupForm({
}, [parameters]);
// Helper function to generate appropriate placeholder text
const getPlaceholder = (parameter) => {
const getPlaceholder = (parameter: ConfigParameter): string => {
// If default is defined and not null, show it
if (parameter.default !== undefined && parameter.default !== null) {
return `Default: ${parameter.default}`;
@@ -2,8 +2,8 @@ import { PROVIDER_REGISTRY } from '../../../ProviderRegistry';
import { Input } from '../../../../../ui/input';
import React from 'react';
import { useState, useEffect } from 'react';
import { Lock, RefreshCw } from 'lucide-react';
import { useState, useEffect, useCallback } from 'react';
import { RefreshCw } from 'lucide-react';
import CustomRadio from '../../../../../ui/CustomRadio';
export default function OllamaForm({ configValues, setConfigValues, provider }) {
@@ -12,12 +12,15 @@ export default function OllamaForm({ configValues, setConfigValues, provider })
const [isCheckingLocal, setIsCheckingLocal] = useState(false);
const [isLocalAvailable, setIsLocalAvailable] = useState(false);
const handleConnectionTypeChange = (value) => {
setConfigValues((prev) => ({
...prev,
connection_type: value,
}));
};
const handleConnectionTypeChange = useCallback(
(value) => {
setConfigValues((prev) => ({
...prev,
connection_type: value,
}));
},
[setConfigValues]
);
// Function to handle input changes and auto-select/deselect the host radio
const handleInputChange = (paramName, value) => {
@@ -40,7 +43,7 @@ export default function OllamaForm({ configValues, setConfigValues, provider })
}
};
const checkLocalAvailability = async () => {
const checkLocalAvailability = useCallback(async () => {
setIsCheckingLocal(true);
// Dummy implementation - simulates checking local availability
@@ -69,12 +72,12 @@ export default function OllamaForm({ configValues, setConfigValues, provider })
} finally {
setIsCheckingLocal(false);
}
};
}, [configValues.connection_type, handleConnectionTypeChange]);
// Check local availability on initial load
useEffect(() => {
checkLocalAvailability();
}, []);
}, [checkLocalAvailability]);
return (
<div className="mt-4 space-y-4">
@@ -1,6 +1,4 @@
import React from 'react';
import CardActions from './CardActions';
import ConfigurationAction from '../interfaces/ConfigurationAction';
interface CardBodyProps {
children: React.ReactNode;
@@ -23,7 +23,7 @@ export const ProviderCard = memo(function ProviderCard({
const providerMetadata: ProviderMetadata | null = provider?.metadata || null;
// Instead of useEffect for logging, use useMemo to memoize the metadata
const metadata = useMemo(() => providerMetadata, [provider]);
const metadata = useMemo(() => providerMetadata, [providerMetadata]);
if (!metadata) {
return <div>ProviderCard error: No metadata provided</div>;
@@ -2,7 +2,7 @@ import React from 'react';
import { Button } from '../../../../ui/button';
import clsx from 'clsx';
import { TooltipWrapper } from './TooltipWrapper';
import { Check, CircleHelp, Plus, RefreshCw, Rocket, Sliders, X } from 'lucide-react';
import { Check, Rocket, Sliders } from 'lucide-react';
interface ActionButtonProps extends React.ComponentProps<typeof Button> {
/** Icon component to render, e.g. `RefreshCw` from lucide-react */
@@ -6,13 +6,11 @@ export function BaseModal({
title,
children,
actions,
onClose,
}: {
isOpen: boolean;
title: string;
children: React.ReactNode;
actions: React.ReactNode; // Buttons for actions
onClose: () => void;
}) {
if (!isOpen) return null;
+11 -11
View File
@@ -1,26 +1,26 @@
import React, { useMemo, useState, useEffect, useRef } from 'react';
import { Buffer } from 'buffer';
import Copy from '../icons/Copy';
import Modal from '../Modal';
import { Card } from '../ui/card';
import { Card } from './card';
interface BotConfig {
instructions?: string;
activities?: string[];
[key: string]: unknown;
}
interface DeepLinkModalProps {
botConfig: any;
botConfig: BotConfig;
onClose: () => void;
onOpen: () => void;
}
// Function to generate a deep link from a bot config
export function generateDeepLink(botConfig: any): string {
export function generateDeepLink(botConfig: BotConfig): string {
const configBase64 = Buffer.from(JSON.stringify(botConfig)).toString('base64');
return `goose://bot?config=${configBase64}`;
}
export function DeepLinkModal({
botConfig: initialBotConfig,
onClose,
onOpen,
}: DeepLinkModalProps) {
export function DeepLinkModal({ botConfig: initialBotConfig, onClose }: DeepLinkModalProps) {
// Create editable state for the bot config
const [botConfig, setBotConfig] = useState(initialBotConfig);
const [instructions, setInstructions] = useState(initialBotConfig.instructions || '');
@@ -61,7 +61,7 @@ export function DeepLinkModal({
instructions,
activities,
});
}, [instructions, activities]);
}, [instructions, activities, botConfig]);
// Handle adding a new activity
const handleAddActivity = () => {