diff --git a/ui/desktop/.eslintrc.json b/ui/desktop/.eslintrc.json
new file mode 100644
index 00000000..5603e4fd
--- /dev/null
+++ b/ui/desktop/.eslintrc.json
@@ -0,0 +1,35 @@
+{
+ "root": true,
+ "env": {
+ "browser": true,
+ "es2020": true
+ },
+ "extends": [
+ "eslint:recommended",
+ "plugin:@typescript-eslint/recommended",
+ "plugin:react-hooks/recommended",
+ "plugin:react/recommended"
+ ],
+ "ignorePatterns": ["dist", ".eslintrc.json"],
+ "parser": "@typescript-eslint/parser",
+ "plugins": ["react-refresh", "@typescript-eslint", "react"],
+ "rules": {
+ "react-refresh/only-export-components": [
+ "warn",
+ { "allowConstantExport": true }
+ ],
+ "@typescript-eslint/no-unused-vars": ["warn", {
+ "varsIgnorePattern": "^React$|^_",
+ "argsIgnorePattern": "^_",
+ "ignoreRestSiblings": true
+ }],
+ "react/react-in-jsx-scope": "off",
+ "react/jsx-uses-react": "off",
+ "react/jsx-uses-vars": "error"
+ },
+ "settings": {
+ "react": {
+ "version": "detect"
+ }
+ }
+}
diff --git a/ui/desktop/index.html b/ui/desktop/index.html
index a871050a..564e1f0e 100644
--- a/ui/desktop/index.html
+++ b/ui/desktop/index.html
@@ -3,20 +3,28 @@
Goose
+
-
diff --git a/ui/desktop/package-lock.json b/ui/desktop/package-lock.json
index 36268ec7..b702e46c 100644
--- a/ui/desktop/package-lock.json
+++ b/ui/desktop/package-lock.json
@@ -62,6 +62,7 @@
"@electron-forge/plugin-fuses": "^7.5.0",
"@electron-forge/plugin-vite": "^7.5.0",
"@electron/fuses": "^1.8.0",
+ "@electron/remote": "^2.1.2",
"@eslint/js": "^8.56.0",
"@hey-api/openapi-ts": "^0.64.4",
"@modelcontextprotocol/sdk": "^1.8.0",
@@ -69,6 +70,7 @@
"@tailwindcss/typography": "^0.5.15",
"@types/cors": "^2.8.17",
"@types/electron-squirrel-startup": "^1.0.2",
+ "@types/electron-window-state": "^2.0.34",
"@types/express": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
@@ -1439,6 +1441,16 @@
"node": ">=12.13.0"
}
},
+ "node_modules/@electron/remote": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/@electron/remote/-/remote-2.1.2.tgz",
+ "integrity": "sha512-EPwNx+nhdrTBxyCqXt/pftoQg/ybtWDW3DUWHafejvnB1ZGGfMpv6e15D8KeempocjXe78T7WreyGGb3mlZxdA==",
+ "dev": true,
+ "license": "MIT",
+ "peerDependencies": {
+ "electron": ">= 13.0.0"
+ }
+ },
"node_modules/@electron/universal": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/@electron/universal/-/universal-2.0.1.tgz",
@@ -4696,6 +4708,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@types/electron-window-state": {
+ "version": "2.0.34",
+ "resolved": "https://registry.npmjs.org/@types/electron-window-state/-/electron-window-state-2.0.34.tgz",
+ "integrity": "sha512-pOJfzi9uoBF9kJa47EXQCizR8g/7gnKs9jLJMMHAWv+oVYVM6Nmgt8/I85z+cdahc9Nn4/f+mHZ7m0sWg0Prbw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "electron": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz",
diff --git a/ui/desktop/package.json b/ui/desktop/package.json
index b9605706..57c35c2e 100644
--- a/ui/desktop/package.json
+++ b/ui/desktop/package.json
@@ -5,6 +5,7 @@
"description": "Goose App",
"main": ".vite/build/main.js",
"scripts": {
+ "typecheck": "tsc --noEmit",
"generate-api": "openapi-ts",
"start-gui": "npm run generate-api && electron-forge start",
"start": "cd ../.. && just run-ui",
@@ -19,8 +20,8 @@
"test-e2e:ui": "npm run generate-api && playwright test --ui",
"test-e2e:debug": "npm run generate-api && playwright test --debug",
"test-e2e:report": "playwright show-report",
- "lint": "eslint \"src/**/*.{ts,tsx}\" --fix",
- "lint:check": "eslint \"src/**/*.{ts,tsx}\"",
+ "lint": "eslint \"src/**/*.{ts,tsx}\" --fix --no-warn-ignored",
+ "lint:check": "eslint \"src/**/*.{ts,tsx}\" --max-warnings 0 --no-warn-ignored",
"format": "prettier --write \"src/**/*.{ts,tsx,css,json}\"",
"format:check": "prettier --check \"src/**/*.{ts,tsx,css,json}\"",
"prepare": "cd ../.. && husky install",
@@ -36,6 +37,7 @@
"@electron-forge/plugin-fuses": "^7.5.0",
"@electron-forge/plugin-vite": "^7.5.0",
"@electron/fuses": "^1.8.0",
+ "@electron/remote": "^2.1.2",
"@eslint/js": "^8.56.0",
"@hey-api/openapi-ts": "^0.64.4",
"@modelcontextprotocol/sdk": "^1.8.0",
@@ -43,6 +45,7 @@
"@tailwindcss/typography": "^0.5.15",
"@types/cors": "^2.8.17",
"@types/electron-squirrel-startup": "^1.0.2",
+ "@types/electron-window-state": "^2.0.34",
"@types/express": "^5.0.0",
"@typescript-eslint/eslint-plugin": "^6.21.0",
"@typescript-eslint/parser": "^6.21.0",
@@ -63,7 +66,7 @@
"license": "Apache-2.0",
"lint-staged": {
"src/**/*.{ts,tsx}": [
- "eslint --fix",
+ "eslint --fix --max-warnings 0 --no-warn-ignored",
"prettier --write"
],
"src/**/*.{css,json}": [
diff --git a/ui/desktop/src/App.tsx b/ui/desktop/src/App.tsx
index 0860eecf..cd13ee1e 100644
--- a/ui/desktop/src/App.tsx
+++ b/ui/desktop/src/App.tsx
@@ -1,4 +1,5 @@
import React, { useEffect, useRef, useState } from 'react';
+import { IpcRendererEvent } from 'electron';
import { addExtensionFromDeepLink } from './extensions';
import { openSharedSessionFromDeepLink } from './sessionLinks';
import { getStoredModel } from './utils/providerUtils';
@@ -15,7 +16,6 @@ import { settingsV2Enabled } from './flags';
import { extractExtensionName } from './components/settings/extensions/utils';
import { GoosehintsModal } from './components/GoosehintsModal';
import { SessionDetails } from './sessions';
-import { SharedSessionDetails } from './sharedSessions';
import WelcomeView from './components/WelcomeView';
import ChatView from './components/ChatView';
@@ -48,14 +48,15 @@ export type View =
| 'sharedSession'
| 'loading';
+export type ViewOptions =
+ | SettingsViewOptions
+ | { resumedSession?: SessionDetails }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ | Record;
+
export type ViewConfig = {
view: View;
- viewOptions?:
- | SettingsViewOptions
- | {
- resumedSession?: SessionDetails;
- }
- | Record;
+ viewOptions?: ViewOptions;
};
export default function App() {
@@ -78,6 +79,12 @@ export default function App() {
return `${cmd} ${args.join(' ')}`.trim();
}
+ const setView = (view: View, viewOptions: ViewOptions = {}) => {
+ console.log(`Setting view to: ${view}`, viewOptions);
+ setInternalView({ view, viewOptions });
+ };
+
+ // Single initialization effect that handles both v1 and v2 settings
useEffect(() => {
if (!settingsV2Enabled) {
return;
@@ -126,7 +133,9 @@ export default function App() {
setView('welcome');
}
} catch (error) {
- setFatalError(`${error.message || 'Unknown error'}`);
+ setFatalError(
+ `Initialization failed: ${error instanceof Error ? error.message : 'Unknown error'}`
+ );
setView('welcome');
}
@@ -136,18 +145,12 @@ export default function App() {
initializeApp().catch((error) => {
console.error('Unhandled error in initialization:', error);
- setFatalError(`${error.message || 'Unknown error'}`);
+ setFatalError(`${error instanceof Error ? error.message : 'Unknown error'}`);
});
- }, []);
-
- const setView = (view: View, viewOptions: Record = {}) => {
- console.log(`Setting view to: ${view}`, viewOptions);
- setInternalView({ view, viewOptions });
- };
+ }, [read, getExtensions, addExtension]);
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
const [isLoadingSession, setIsLoadingSession] = useState(false);
- const [sharedSession, setSharedSession] = useState(null);
const [sharedSessionError, setSharedSessionError] = useState(null);
const [isLoadingSharedSession, setIsLoadingSharedSession] = useState(false);
const { chat, setChat } = useChat({ setView, setIsLoadingSession });
@@ -158,13 +161,15 @@ export default function App() {
window.electron.reactReady();
} catch (error) {
console.error('Error sending reactReady:', error);
- setFatalError(`React ready notification failed: ${error.message}`);
+ setFatalError(
+ `React ready notification failed: ${error instanceof Error ? error.message : 'Unknown error'}`
+ );
}
}, []);
// Handle shared session deep links
useEffect(() => {
- const handleOpenSharedSession = async (_: any, link: string) => {
+ const handleOpenSharedSession = async (_event: IpcRendererEvent, link: string) => {
window.electron.logInfo(`Opening shared session from deep link ${link}`);
setIsLoadingSharedSession(true);
setSharedSessionError(null);
@@ -196,7 +201,7 @@ export default function App() {
try {
const workingDir = window.appConfig.get('GOOSE_WORKING_DIR');
console.log(`Creating new chat window with working dir: ${workingDir}`);
- window.electron.createChatWindow(undefined, workingDir);
+ window.electron.createChatWindow(undefined, workingDir as string);
} catch (error) {
console.error('Error creating new window:', error);
}
@@ -211,7 +216,7 @@ export default function App() {
useEffect(() => {
console.log('Setting up fatal error handler');
- const handleFatalError = (_: any, errorMessage: string) => {
+ const handleFatalError = (_event: IpcRendererEvent, errorMessage: string) => {
console.error('Encountered a fatal error: ', errorMessage);
// Log additional context that might help diagnose the issue
console.error('Current view:', view);
@@ -227,7 +232,7 @@ export default function App() {
useEffect(() => {
console.log('Setting up view change handler');
- const handleSetView = (_, newView) => {
+ const handleSetView = (_event: IpcRendererEvent, newView: View) => {
console.log(`Received view change request to: ${newView}`);
setView(newView);
};
@@ -248,7 +253,7 @@ export default function App() {
// TODO: modify
useEffect(() => {
console.log('Setting up extension handler');
- const handleAddExtension = (_: any, link: string) => {
+ const handleAddExtension = (_event: IpcRendererEvent, link: string) => {
try {
console.log(`Received add-extension event with link: ${link}`);
const command = extractCommand(link);
@@ -298,7 +303,8 @@ export default function App() {
setPendingLink(null);
};
- // TODO: remove
+ // TODO: remove -- careful removal of these and the useEffect below breaks
+ // reloading to chat view using stored provider
const { switchModel } = useModel(); // TODO: remove
const { addRecentModel } = useRecentModels(); // TODO: remove
@@ -323,9 +329,9 @@ export default function App() {
} else {
setView('welcome');
}
- } catch (err) {
- console.error('DETECTION ERROR:', err);
- setFatalError(`Config detection error: ${err.message || 'Unknown error'}`);
+ } catch (error) {
+ console.error('DETECTION ERROR:', error);
+ setFatalError(`Config detection error: ${error.message || 'Unknown error'}`);
}
};
@@ -360,18 +366,19 @@ export default function App() {
setFatalError(`Initialization failed: ${error.message || 'Unknown error'}`);
}
}
- } catch (err) {
- console.error('SETUP ERROR:', err);
- setFatalError(`Setup error: ${err.message || 'Unknown error'}`);
+ } catch (error) {
+ console.error('SETUP ERROR:', error);
+ setFatalError(`Setup error: ${error.message || 'Unknown error'}`);
}
};
// Execute the functions with better error handling
detectStoredProvider();
- setupStoredProvider().catch((err) => {
- console.error('ASYNC SETUP ERROR:', err);
- setFatalError(`Async setup error: ${err.message || 'Unknown error'}`);
+ setupStoredProvider().catch((error) => {
+ console.error('ASYNC SETUP ERROR:', error);
+ setFatalError(`Async setup error: ${error.message || 'Unknown error'}`);
});
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
if (fatalError) {
@@ -472,12 +479,12 @@ export default function App() {
{view === 'sessions' && }
{view === 'sharedSession' && (
setView('sessions')}
onRetry={async () => {
- if (viewOptions.shareToken && viewOptions.baseUrl) {
+ if (viewOptions?.shareToken && viewOptions?.baseUrl) {
setIsLoadingSharedSession(true);
try {
await openSharedSessionFromDeepLink(
@@ -498,7 +505,7 @@ export default function App() {
{isGoosehintsModalOpen && (
)}
diff --git a/ui/desktop/src/components/BottomMenu.tsx b/ui/desktop/src/components/BottomMenu.tsx
index 6cd23cf7..4a1bc1b8 100644
--- a/ui/desktop/src/components/BottomMenu.tsx
+++ b/ui/desktop/src/components/BottomMenu.tsx
@@ -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(null);
// Add effect to handle clicks outside
diff --git a/ui/desktop/src/components/BottomMenuModeSelection.tsx b/ui/desktop/src/components/BottomMenuModeSelection.tsx
index f99d480c..9f2eaeaa 100644
--- a/ui/desktop/src/components/BottomMenuModeSelection.tsx
+++ b/ui/desktop/src/components/BottomMenuModeSelection.tsx
@@ -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(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) => {
diff --git a/ui/desktop/src/components/ChatView.tsx b/ui/desktop/src/components/ChatView.tsx
index cd2599c8..4826257a 100644
--- a/ui/desktop/src/components/ChatView.tsx
+++ b/ui/desktop/src/components/ChatView.tsx
@@ -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) => void;
+ setView: (view: View, viewOptions?: ViewOptions) => void;
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
}) {
- const [messageMetadata, setMessageMetadata] = useState>({});
+ // Disabled askAi calls to save costs
+ // const [messageMetadata, setMessageMetadata] = useState>({});
const [hasMessages, setHasMessages] = useState(false);
const [lastInteractionTime, setLastInteractionTime] = useState(Date.now());
const [showGame, setShowGame] = useState(false);
const [waitingForAgentResponse, setWaitingForAgentResponse] = useState(false);
const [showShareableBotModal, setshowShareableBotModal] = useState(false);
- const [generatedBotConfig, setGeneratedBotConfig] = useState(null);
+ const [generatedBotConfig, setGeneratedBotConfig] = useState(null);
const scrollRef = useRef(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((history, message) => {
@@ -372,7 +378,7 @@ export default function ChatView({
return history;
}, [])
.reverse();
- }, [filteredMessages, isUserMessage]);
+ }, [filteredMessages]);
return (
@@ -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];
diff --git a/ui/desktop/src/components/ConfigContext.tsx b/ui/desktop/src/components/ConfigContext.tsx
index 9d81c5e2..9359de80 100644
--- a/ui/desktop/src/components/ConfigContext.tsx
+++ b/ui/desktop/src/components/ConfigContext.tsx
@@ -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
= ({ children }) => {
const [providersList, setProvidersList] = useState([]);
const [extensionsList, setExtensionsList] = useState([]);
+ 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 => {
+ 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 => {
+ 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 = ({ 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 => {
- 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 => {
- // 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,
diff --git a/ui/desktop/src/components/FlappyGoose.tsx b/ui/desktop/src/components/FlappyGoose.tsx
index 8b9a7a1c..3cd8b589 100644
--- a/ui/desktop/src/components/FlappyGoose.tsx
+++ b/ui/desktop/src/components/FlappyGoose.tsx
@@ -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 = ({ 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 = ({ 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 = ({ onClose }) => {
window.removeEventListener('keydown', handleKeyPress);
gameState.current.running = false;
};
- }, [imagesReady]);
+ }, [CANVAS_HEIGHT, CANVAS_WIDTH, flap, imagesReady, startGame]);
return (
{
// 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 (
diff --git a/ui/desktop/src/components/Input.tsx b/ui/desktop/src/components/Input.tsx
index bf8492a3..248923be 100644
--- a/ui/desktop/src/components/Input.tsx
+++ b/ui/desktop/src/components/Input.tsx
@@ -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
) => {
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) => {
+ const handleCompositionStart = () => {
setIsComposing(true);
};
- const handleCompositionEnd = (evt: React.CompositionEvent) => {
+ 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"
>
diff --git a/ui/desktop/src/components/MarkdownContent.tsx b/ui/desktop/src/components/MarkdownContent.tsx
index 26c63fe3..ca7fe384 100644
--- a/ui/desktop/src/components/MarkdownContent.tsx
+++ b/ui/desktop/src/components/MarkdownContent.tsx
@@ -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 (
,
- code({ node, className, children, inlinecode, ...props }) {
+ a: ({ ...props }) => ,
+ code({ className, children, inlinecode, ...props }) {
const match = /language-(\w+)/.exec(className || 'language-text');
return inlinecode == 'false' && match ? (
{String(children).replace(/\n$/, '')}
diff --git a/ui/desktop/src/components/ToolCallArguments.tsx b/ui/desktop/src/components/ToolCallArguments.tsx
index 3c06736a..dbff369e 100644
--- a/ui/desktop/src/components/ToolCallArguments.tsx
+++ b/ui/desktop/src/components/ToolCallArguments.tsx
@@ -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;
+ args: Record;
}
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 ? '▼ ' : '▶ '} */}
- {/* {isExpanded && (
-
-
-
- )} */}
);
}
diff --git a/ui/desktop/src/components/conversation/SearchBar.tsx b/ui/desktop/src/components/conversation/SearchBar.tsx
index 26861c05..595da07a 100644
--- a/ui/desktop/src/components/conversation/SearchBar.tsx
+++ b/ui/desktop/src/components/conversation/SearchBar.tsx
@@ -44,21 +44,25 @@ export const SearchBar: React.FC = ({
// 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) => {
const value = event.target.value;
setDisplayTerm(value); // Update display immediately
@@ -93,7 +97,7 @@ export const SearchBar: React.FC = ({
const handleClose = () => {
setIsExiting(true);
- debouncedSearch.cancel(); // Cancel any pending searches
+ debouncedSearch.cancel?.(); // Cancel any pending searches
setTimeout(() => {
onClose();
}, 150); // Match animation duration
diff --git a/ui/desktop/src/components/conversation/SearchView.tsx b/ui/desktop/src/components/conversation/SearchView.tsx
index a08e6562..8f5d53a1 100644
--- a/ui/desktop/src/components/conversation/SearchView.tsx
+++ b/ui/desktop/src/components/conversation/SearchView.tsx
@@ -36,20 +36,25 @@ export const SearchView: React.FC> = ({
// 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> = ({
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> = ({
highlighterRef.current.clearHighlights();
}
// Cancel any pending highlight operations
- debouncedHighlight.cancel();
+ debouncedHighlight.cancel?.();
};
return (
diff --git a/ui/desktop/src/components/more_menu/MoreMenu.tsx b/ui/desktop/src/components/more_menu/MoreMenu.tsx
index 12a1d207..973efd45 100644
--- a/ui/desktop/src/components/more_menu/MoreMenu.tsx
+++ b/ui/desktop/src/components/more_menu/MoreMenu.tsx
@@ -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(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({
diff --git a/ui/desktop/src/components/more_menu/MoreMenuLayout.tsx b/ui/desktop/src/components/more_menu/MoreMenuLayout.tsx
index 91f5a485..f697b690 100644
--- a/ui/desktop/src/components/more_menu/MoreMenuLayout.tsx
+++ b/ui/desktop/src/components/more_menu/MoreMenuLayout.tsx
@@ -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) => void;
+ setView: (view: View, viewOptions?: ViewOptions) => void;
setIsGoosehintsModalOpen: (isOpen: boolean) => void;
}) {
return (
diff --git a/ui/desktop/src/components/sessions/SessionHistoryView.tsx b/ui/desktop/src/components/sessions/SessionHistoryView.tsx
index d67e87ec..8601229c 100644
--- a/ui/desktop/src/components/sessions/SessionHistoryView.tsx
+++ b/ui/desktop/src/components/sessions/SessionHistoryView.tsx
@@ -39,7 +39,6 @@ const SessionHistoryView: React.FC = ({
const [isSharing, setIsSharing] = useState(false);
const [isCopied, setIsCopied] = useState(false);
const [canShare, setCanShare] = useState(false);
- const [shareError, setShareError] = useState(null);
useEffect(() => {
const savedSessionConfig = localStorage.getItem('session_sharing_config');
@@ -58,7 +57,6 @@ const SessionHistoryView: React.FC = ({
const handleShare = async () => {
setIsSharing(true);
- setShareError(null);
try {
// Get the session sharing configuration from localStorage
@@ -87,7 +85,6 @@ const SessionHistoryView: React.FC = ({
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'}`
);
diff --git a/ui/desktop/src/components/sessions/SessionListView.tsx b/ui/desktop/src/components/sessions/SessionListView.tsx
index 616b731b..5ad2cec7 100644
--- a/ui/desktop/src/components/sessions/SessionListView.tsx
+++ b/ui/desktop/src/components/sessions/SessionListView.tsx
@@ -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) => void;
+ setView: (view: View, viewOptions?: ViewOptions) => void;
onSelectSession: (sessionId: string) => void;
}
diff --git a/ui/desktop/src/components/sessions/SessionsView.tsx b/ui/desktop/src/components/sessions/SessionsView.tsx
index 8e8d9d6b..c28cd7be 100644
--- a/ui/desktop/src/components/sessions/SessionsView.tsx
+++ b/ui/desktop/src/components/sessions/SessionsView.tsx
@@ -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) => void;
+ setView: (view: View, viewOptions?: ViewOptions) => void;
}
const SessionsView: React.FC = ({ setView }) => {
diff --git a/ui/desktop/src/components/settings/OllamaBattleGame.tsx b/ui/desktop/src/components/settings/OllamaBattleGame.tsx
index d251ce63..c3b98f21 100644
--- a/ui/desktop/src/components/settings/OllamaBattleGame.tsx
+++ b/ui/desktop/src/components/settings/OllamaBattleGame.tsx
@@ -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(null);
+ // Use Audio element type for audioRef
+ const audioRef = useRef<{ play: () => Promise; pause: () => void; volume: number } | null>(
+ null
+ );
const [isMuted, setIsMuted] = useState(false);
const [battleState, setBattleState] = useState({
diff --git a/ui/desktop/src/components/settings/SettingsView.tsx b/ui/desktop/src/components/settings/SettingsView.tsx
index 0d048ab3..e0f8c2ea 100644
--- a/ui/desktop/src/components/settings/SettingsView.tsx
+++ b/ui/desktop/src/components/settings/SettingsView.tsx
@@ -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(() => {
@@ -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);
diff --git a/ui/desktop/src/components/settings/api_keys/utils.tsx b/ui/desktop/src/components/settings/api_keys/utils.tsx
index e3847684..8ae62fef 100644
--- a/ui/desktop/src/components/settings/api_keys/utils.tsx
+++ b/ui/desktop/src/components/settings/api_keys/utils.tsx
@@ -101,8 +101,18 @@ export async function getProvidersList(): Promise {
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
diff --git a/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx b/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx
index 637c2389..db03a5a3 100644
--- a/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx
+++ b/ui/desktop/src/components/settings/extensions/ManualExtensionModal.tsx
@@ -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';
diff --git a/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx b/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx
index c682bf34..668974cc 100644
--- a/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx
+++ b/ui/desktop/src/components/settings/models/hardcoded_stuff.tsx
@@ -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 = [
diff --git a/ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx b/ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx
index bce08f64..7f39eb82 100644
--- a/ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx
+++ b/ui/desktop/src/components/settings/providers/BaseProviderGrid.tsx
@@ -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 (
diff --git a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx
index ab3c86b6..7c6d9e09 100644
--- a/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx
+++ b/ui/desktop/src/components/settings/providers/ConfigureProvidersGrid.tsx
@@ -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';
diff --git a/ui/desktop/src/components/settings_v2/SettingsView.tsx b/ui/desktop/src/components/settings_v2/SettingsView.tsx
index 78976eff..3d797ec1 100644
--- a/ui/desktop/src/components/settings_v2/SettingsView.tsx
+++ b/ui/desktop/src/components/settings_v2/SettingsView.tsx
@@ -15,11 +15,9 @@ export type SettingsViewOptions = {
export default function SettingsView({
onClose,
setView,
- viewOptions,
}: {
onClose: () => void;
setView: (view: View) => void;
- viewOptions: SettingsViewOptions;
}) {
return (
diff --git a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx
index 86763ff7..d21e1008 100644
--- a/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx
+++ b/ui/desktop/src/components/settings_v2/extensions/ExtensionsSection.tsx
@@ -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
(true);
- const [error, setError] = useState(null);
const [extensions, setExtensions] = useState([]);
const [selectedExtension, setSelectedExtension] = useState(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,
diff --git a/ui/desktop/src/components/settings_v2/extensions/agent-api.ts b/ui/desktop/src/components/settings_v2/extensions/agent-api.ts
index 29102df5..243a91c2 100644
--- a/ui/desktop/src/components/settings_v2/extensions/agent-api.ts
+++ b/ui/desktop/src/components/settings_v2/extensions/agent-api.ts
@@ -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 {
// Configure toast notifications
@@ -118,7 +123,7 @@ function handleErrorResponse(
}
// Safely parses JSON response
-async function parseResponseData(response: Response): Promise {
+async function parseResponseData(response: Response): Promise {
try {
const text = await response.text();
return text ? JSON.parse(text) : { error: false };
diff --git a/ui/desktop/src/components/settings_v2/extensions/extension-manager.ts b/ui/desktop/src/components/settings_v2/extensions/extension-manager.ts
index e558f89f..bd0f4113 100644
--- a/ui/desktop/src/components/settings_v2/extensions/extension-manager.ts
+++ b/ui/desktop/src/components/settings_v2/extensions/extension-manager.ts
@@ -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(fn: () => Promise, options: RetryOptions = {}): Promise {
+ 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(fn: () => Promise, options: RetryOptions = {}): Promise {
- 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;
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 {
diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx
index 4f9bd7f4..36fa09cc 100644
--- a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx
+++ b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionConfigFields.tsx
@@ -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;
}
diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionInfoFields.tsx b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionInfoFields.tsx
index 7433a2df..4dcc91a3 100644
--- a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionInfoFields.tsx
+++ b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionInfoFields.tsx
@@ -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;
}
diff --git a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionTimeoutField.tsx b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionTimeoutField.tsx
index 7581db75..1a0aae42 100644
--- a/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionTimeoutField.tsx
+++ b/ui/desktop/src/components/settings_v2/extensions/modal/ExtensionTimeoutField.tsx
@@ -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;
}
diff --git a/ui/desktop/src/components/settings_v2/extensions/utils.ts b/ui/desktop/src/components/settings_v2/extensions/utils.ts
index dc7989b7..f06faa26 100644
--- a/ui/desktop/src/components/settings_v2/extensions/utils.ts
+++ b/ui/desktop/src/components/settings_v2/extensions/utils.ts
@@ -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;
}
diff --git a/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx b/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx
index bf462220..7d89bdff 100644
--- a/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx
+++ b/ui/desktop/src/components/settings_v2/mode/ModeSection.tsx
@@ -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');
diff --git a/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx b/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx
index c8b3f6c7..a96c2fb5 100644
--- a/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx
+++ b/ui/desktop/src/components/settings_v2/models/ModelsSection.tsx
@@ -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 (
diff --git a/ui/desktop/src/components/settings_v2/models/bottom_bar/ModelsBottomBar.tsx b/ui/desktop/src/components/settings_v2/models/bottom_bar/ModelsBottomBar.tsx
index 4e9c7941..ba212bc5 100644
--- a/ui/desktop/src/components/settings_v2/models/bottom_bar/ModelsBottomBar.tsx
+++ b/ui/desktop/src/components/settings_v2/models/bottom_bar/ModelsBottomBar.tsx
@@ -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;
setView: (view: View) => void;
}
export default function ModelsBottomBar({ dropdownRef, setView }: ModelsBottomBarProps) {
diff --git a/ui/desktop/src/components/settings_v2/models/model_list/BaseModelsList.tsx b/ui/desktop/src/components/settings_v2/models/model_list/BaseModelsList.tsx
index 37b1c1fd..12512fda 100644
--- a/ui/desktop/src/components/settings_v2/models/model_list/BaseModelsList.tsx
+++ b/ui/desktop/src/components/settings_v2/models/model_list/BaseModelsList.tsx
@@ -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 });
diff --git a/ui/desktop/src/components/settings_v2/models/model_list/recentModels.ts b/ui/desktop/src/components/settings_v2/models/model_list/recentModels.ts
index 07100b78..9490d06a 100644
--- a/ui/desktop/src/components/settings_v2/models/model_list/recentModels.ts
+++ b/ui/desktop/src/components/settings_v2/models/model_list/recentModels.ts
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react';
+import { useEffect, useState } from 'react';
import Model from '../modelInterface';
const MAX_RECENT_MODELS = 3;
diff --git a/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx
index 8fb88a23..fac53168 100644
--- a/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx
+++ b/ui/desktop/src/components/settings_v2/models/subcomponents/AddModelModal.tsx
@@ -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 }) => (
{
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 () => {
diff --git a/ui/desktop/src/components/settings_v2/providers/ProviderGrid.tsx b/ui/desktop/src/components/settings_v2/providers/ProviderGrid.tsx
index 9537ef0d..68a26def 100644
--- a/ui/desktop/src/components/settings_v2/providers/ProviderGrid.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/ProviderGrid.tsx
@@ -72,7 +72,13 @@ const ProviderCards = memo(function ProviderCards({
isOnboarding={isOnboarding}
/>
));
- }, [providers, isOnboarding, configureProviderViaModal, onProviderLaunch]);
+ }, [
+ providers,
+ isOnboarding,
+ configureProviderViaModal,
+ deleteProviderConfigViaModal,
+ onProviderLaunch,
+ ]);
return <>{providerCards}>;
});
diff --git a/ui/desktop/src/components/settings_v2/providers/ProviderRegistry.tsx b/ui/desktop/src/components/settings_v2/providers/ProviderRegistry.tsx
index 822b79f8..e2232d67 100644
--- a/ui/desktop/src/components/settings_v2/providers/ProviderRegistry.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/ProviderRegistry.tsx
@@ -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;
diff --git a/ui/desktop/src/components/settings_v2/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings_v2/providers/ProviderSettingsPage.tsx
index 05190f3f..ddbc9406 100644
--- a/ui/desktop/src/components/settings_v2/providers/ProviderSettingsPage.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/ProviderSettingsPage.tsx
@@ -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 (
diff --git a/ui/desktop/src/components/settings_v2/providers/interfaces/ProviderDetails.tsx b/ui/desktop/src/components/settings_v2/providers/interfaces/ProviderDetails.tsx
index 154a1703..22abe7ab 100644
--- a/ui/desktop/src/components/settings_v2/providers/interfaces/ProviderDetails.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/interfaces/ProviderDetails.tsx
@@ -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;
- customSubmit?: (e: any) => void;
+ customSubmit?: (e: React.SyntheticEvent) => void;
}
diff --git a/ui/desktop/src/components/settings_v2/providers/interfaces/ProviderState.tsx b/ui/desktop/src/components/settings_v2/providers/interfaces/ProviderState.tsx
index f4720679..6a31f950 100644
--- a/ui/desktop/src/components/settings_v2/providers/interfaces/ProviderState.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/interfaces/ProviderState.tsx
@@ -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;
}
diff --git a/ui/desktop/src/components/settings_v2/providers/modal/ProviderModalProvider.tsx b/ui/desktop/src/components/settings_v2/providers/modal/ProviderModalProvider.tsx
index 79c9aee5..5c1a48a9 100644
--- a/ui/desktop/src/components/settings_v2/providers/modal/ProviderModalProvider.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/modal/ProviderModalProvider.tsx
@@ -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 {
diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/ProviderSetupActions.tsx b/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/ProviderSetupActions.tsx
index d2309d62..7556c2da 100644
--- a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/ProviderSetupActions.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/ProviderSetupActions.tsx
@@ -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;
diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx b/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx
index 09ab4b50..cf71f8b7 100644
--- a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/DefaultProviderSetupForm.tsx
@@ -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;
- setConfigValues: React.Dispatch>>;
- provider: any;
- validationErrors: any;
+ configValues: Record;
+ setConfigValues: React.Dispatch>>;
+ 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}`;
diff --git a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/OllamaForm.tsx b/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/OllamaForm.tsx
index 7acded28..6a2b93af 100644
--- a/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/OllamaForm.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/modal/subcomponents/forms/OllamaForm.tsx
@@ -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 (
diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/CardBody.tsx b/ui/desktop/src/components/settings_v2/providers/subcomponents/CardBody.tsx
index b5f2c1ed..30f881ae 100644
--- a/ui/desktop/src/components/settings_v2/providers/subcomponents/CardBody.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/subcomponents/CardBody.tsx
@@ -1,6 +1,4 @@
import React from 'react';
-import CardActions from './CardActions';
-import ConfigurationAction from '../interfaces/ConfigurationAction';
interface CardBodyProps {
children: React.ReactNode;
diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/ProviderCard.tsx b/ui/desktop/src/components/settings_v2/providers/subcomponents/ProviderCard.tsx
index 017b6a04..a3b0d0ca 100644
--- a/ui/desktop/src/components/settings_v2/providers/subcomponents/ProviderCard.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/subcomponents/ProviderCard.tsx
@@ -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
ProviderCard error: No metadata provided
;
diff --git a/ui/desktop/src/components/settings_v2/providers/subcomponents/buttons/CardButtons.tsx b/ui/desktop/src/components/settings_v2/providers/subcomponents/buttons/CardButtons.tsx
index a6476edb..8d8b2397 100644
--- a/ui/desktop/src/components/settings_v2/providers/subcomponents/buttons/CardButtons.tsx
+++ b/ui/desktop/src/components/settings_v2/providers/subcomponents/buttons/CardButtons.tsx
@@ -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
{
/** Icon component to render, e.g. `RefreshCw` from lucide-react */
diff --git a/ui/desktop/src/components/ui/BaseModal.tsx b/ui/desktop/src/components/ui/BaseModal.tsx
index 93ab8873..1550c822 100644
--- a/ui/desktop/src/components/ui/BaseModal.tsx
+++ b/ui/desktop/src/components/ui/BaseModal.tsx
@@ -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;
diff --git a/ui/desktop/src/components/ui/DeepLinkModal.tsx b/ui/desktop/src/components/ui/DeepLinkModal.tsx
index 3d18b5f7..db7beca5 100644
--- a/ui/desktop/src/components/ui/DeepLinkModal.tsx
+++ b/ui/desktop/src/components/ui/DeepLinkModal.tsx
@@ -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 = () => {
diff --git a/ui/desktop/src/extensions.tsx b/ui/desktop/src/extensions.tsx
index b9ebc871..67cdeb40 100644
--- a/ui/desktop/src/extensions.tsx
+++ b/ui/desktop/src/extensions.tsx
@@ -1,4 +1,3 @@
-import React from 'react';
import { getApiUrl, getSecretKey } from './config';
import { type View } from './App';
import { type SettingsViewOptions } from './components/settings/SettingsView';
@@ -6,7 +5,6 @@ import { toast } from 'react-toastify';
import builtInExtensionsData from './built-in-extensions.json';
import { toastError, toastLoading, toastSuccess } from './toasts';
-import { Toast } from 'react-toastify/dist/components';
// Hardcoded default extension timeout in seconds
export const DEFAULT_EXTENSION_TIMEOUT = 300;
@@ -214,7 +212,9 @@ export async function loadAndAddStoredExtensions() {
if (userSettingsStr) {
const userSettings = JSON.parse(userSettingsStr);
- const enabledExtensions = userSettings.extensions.filter((ext: any) => ext.enabled);
+ const enabledExtensions = userSettings.extensions.filter(
+ (ext: FullExtensionConfig) => ext.enabled
+ );
console.log('Adding extensions from localStorage: ', enabledExtensions);
for (const ext of enabledExtensions) {
await addExtension(ext, true);
diff --git a/ui/desktop/src/goosed.ts b/ui/desktop/src/goosed.ts
index e857c55a..1424e0d6 100644
--- a/ui/desktop/src/goosed.ts
+++ b/ui/desktop/src/goosed.ts
@@ -5,7 +5,9 @@ import path from 'node:path';
import { getBinaryPath } from './utils/binaryPath';
import log from './utils/logger';
import { ChildProcessByStdio } from 'node:child_process';
-import { Readable } from 'node:stream';
+import { Readable, Buffer } from 'node:stream';
+import { App } from 'electron';
+import type { ProcessEnv } from 'node:process';
// Find an available port to start goosed on
export const findAvailablePort = (): Promise => {
@@ -50,10 +52,20 @@ const checkServerStatus = async (
return false;
};
+interface GooseProcessEnv extends ProcessEnv {
+ HOME: string;
+ USERPROFILE: string;
+ APPDATA: string;
+ LOCALAPPDATA: string;
+ PATH: string;
+ GOOSE_PORT: string;
+ GOOSE_SERVER__SECRET_KEY?: string;
+}
+
export const startGoosed = async (
- app,
- dir = null,
- env = {}
+ app: App,
+ dir: string | null = null,
+ env: Partial = {}
): Promise<[number, string, ChildProcessByStdio]> => {
// we default to running goosed in home dir - if not specified
const homeDir = os.homedir();
@@ -72,7 +84,7 @@ export const startGoosed = async (
log.info(`Starting goosed from: ${goosedPath} on port ${port} in dir ${dir}`);
// Define additional environment variables
- const additionalEnv = {
+ const additionalEnv: GooseProcessEnv = {
// Set HOME for UNIX-like systems
HOME: homeDir,
// Set USERPROFILE for Windows
@@ -82,16 +94,16 @@ export const startGoosed = async (
// Set LOCAL_APPDATA for Windows
LOCALAPPDATA: process.env.LOCALAPPDATA || path.join(homeDir, 'AppData', 'Local'),
// Set PATH to include the binary directory
- PATH: `${path.dirname(goosedPath)}${path.delimiter}${process.env.PATH}`,
+ PATH: `${path.dirname(goosedPath)}${path.delimiter}${process.env.PATH || ''}`,
// start with the port specified
GOOSE_PORT: String(port),
GOOSE_SERVER__SECRET_KEY: process.env.GOOSE_SERVER__SECRET_KEY,
// Add any additional environment variables passed in
...env,
- };
+ } as GooseProcessEnv;
// Merge parent environment with additional environment variables
- const processEnv = { ...process.env, ...additionalEnv };
+ const processEnv: GooseProcessEnv = { ...process.env, ...additionalEnv } as GooseProcessEnv;
// Add detailed logging for troubleshooting
log.info(`Process platform: ${process.platform}`);
@@ -111,6 +123,7 @@ export const startGoosed = async (
// Verify binary exists
try {
+ // eslint-disable-next-line @typescript-eslint/no-var-requires
const fs = require('fs');
const stats = fs.statSync(goosedPath);
log.info(`Binary exists: ${stats.isFile()}`);
@@ -122,7 +135,7 @@ export const startGoosed = async (
const spawnOptions = {
cwd: dir,
env: processEnv,
- stdio: ['ignore', 'pipe', 'pipe'],
+ stdio: ['ignore', 'pipe', 'pipe'] as ['ignore', 'pipe', 'pipe'],
// Hide terminal window on Windows
windowsHide: true,
// Run detached on Windows only to avoid terminal windows
@@ -142,19 +155,19 @@ export const startGoosed = async (
goosedProcess.unref();
}
- goosedProcess.stdout.on('data', (data) => {
+ goosedProcess.stdout.on('data', (data: Buffer) => {
log.info(`goosed stdout for port ${port} and dir ${dir}: ${data.toString()}`);
});
- goosedProcess.stderr.on('data', (data) => {
+ goosedProcess.stderr.on('data', (data: Buffer) => {
log.error(`goosed stderr for port ${port} and dir ${dir}: ${data.toString()}`);
});
- goosedProcess.on('close', (code) => {
+ goosedProcess.on('close', (code: number | null) => {
log.info(`goosed process exited with code ${code} for port ${port} and dir ${dir}`);
});
- goosedProcess.on('error', (err) => {
+ goosedProcess.on('error', (err: Error) => {
log.error(`Failed to start goosed on port ${port} and dir ${dir}`, err);
throw err; // Propagate the error
});
diff --git a/ui/desktop/src/hooks/useChat.ts b/ui/desktop/src/hooks/useChat.ts
index 44cf4f9e..35027b8a 100644
--- a/ui/desktop/src/hooks/useChat.ts
+++ b/ui/desktop/src/hooks/useChat.ts
@@ -1,10 +1,11 @@
import { useEffect, useState } from 'react';
import { ChatType } from '../components/ChatView';
import { fetchSessionDetails, generateSessionId } from '../sessions';
+import { View, ViewOptions } from '../App';
type UseChatArgs = {
setIsLoadingSession: (isLoading: boolean) => void;
- setView: (view: string) => void;
+ setView: (view: View, viewOptions?: ViewOptions) => void;
};
export const useChat = ({ setIsLoadingSession, setView }: UseChatArgs) => {
const [chat, setChat] = useState({
@@ -49,6 +50,8 @@ export const useChat = ({ setIsLoadingSession, setView }: UseChatArgs) => {
};
checkForResumeSession();
+ // todo: rework this to allow for exhaustive deps currently throws app in error loop
+ // eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return { chat, setChat };
diff --git a/ui/desktop/src/json.d.ts b/ui/desktop/src/json.d.ts
index bbab69d2..d60d46d6 100644
--- a/ui/desktop/src/json.d.ts
+++ b/ui/desktop/src/json.d.ts
@@ -1,4 +1,4 @@
declare module '*.json' {
- const value: any;
+ const value: Record;
export default value;
}
diff --git a/ui/desktop/src/main.ts b/ui/desktop/src/main.ts
index 2384f258..02e9c5b3 100644
--- a/ui/desktop/src/main.ts
+++ b/ui/desktop/src/main.ts
@@ -3,13 +3,13 @@ import {
session,
BrowserWindow,
dialog,
- globalShortcut,
ipcMain,
Menu,
MenuItem,
Notification,
powerSaveBlocker,
Tray,
+ App,
} from 'electron';
import { Buffer } from 'node:buffer';
import started from 'electron-squirrel-startup';
@@ -41,8 +41,8 @@ app.setAsDefaultProtocolClient('goose');
// Triggered when the user opens "goose://..." links
let firstOpenWindow: BrowserWindow;
-let pendingDeepLink = null; // Store deep link if sent before React is ready
-app.on('open-url', async (event, url) => {
+let pendingDeepLink: string | null = null; // Store deep link if sent before React is ready
+app.on('open-url', async (_event, url) => {
pendingDeepLink = url;
// Parse the URL to determine the type
@@ -156,13 +156,21 @@ let appConfig = {
let windowCounter = 0;
const windowMap = new Map();
+interface BotConfig {
+ id: string;
+ name: string;
+ description: string;
+ instructions: string;
+ activities: string[];
+}
+
const createChat = async (
- app,
+ app: App,
query?: string,
dir?: string,
version?: string,
resumeSessionId?: string,
- botConfig?: any // Bot configuration
+ botConfig?: BotConfig
) => {
// Apply current environment settings before creating chat
updateEnvironmentVariables(envToggles);
@@ -202,7 +210,7 @@ const createChat = async (
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
// Open all links in external browser
if (url.startsWith('http:') || url.startsWith('https:')) {
- require('electron').shell.openExternal(url);
+ electron.shell.openExternal(url);
return { action: 'deny' };
}
return { action: 'allow' };
@@ -350,7 +358,7 @@ const openDirectoryDialog = async (replaceWindow: boolean = false) => {
if (!result.canceled && result.filePaths.length > 0) {
addRecentDir(result.filePaths[0]);
const currentWindow = BrowserWindow.getFocusedWindow();
- const newWindow = await createChat(app, undefined, result.filePaths[0]);
+ await createChat(app, undefined, result.filePaths[0]);
if (replaceWindow) {
currentWindow.close();
}
@@ -376,7 +384,7 @@ process.on('unhandledRejection', (error) => {
handleFatalError(error instanceof Error ? error : new Error(String(error)));
});
-ipcMain.on('react-ready', (event) => {
+ipcMain.on('react-ready', () => {
console.log('React ready event received');
if (pendingDeepLink) {
@@ -399,7 +407,7 @@ ipcMain.on('react-ready', (event) => {
});
// Handle directory chooser
-ipcMain.handle('directory-chooser', (_, replace: boolean = false) => {
+ipcMain.handle('directory-chooser', (_event, replace: boolean = false) => {
return openDirectoryDialog(replace);
});
@@ -447,11 +455,11 @@ ipcMain.handle('check-ollama', async () => {
});
// Handle binary path requests
-ipcMain.handle('get-binary-path', (event, binaryName) => {
+ipcMain.handle('get-binary-path', (_event, binaryName) => {
return getBinaryPath(app, binaryName);
});
-ipcMain.handle('read-file', (event, filePath) => {
+ipcMain.handle('read-file', (_event, filePath) => {
return new Promise((resolve) => {
exec(`cat ${filePath}`, (error, stdout, stderr) => {
if (error) {
@@ -467,7 +475,7 @@ ipcMain.handle('read-file', (event, filePath) => {
});
});
-ipcMain.handle('write-file', (event, filePath, content) => {
+ipcMain.handle('write-file', (_event, filePath, content) => {
return new Promise((resolve) => {
const command = `cat << 'EOT' > ${filePath}
${content}
@@ -513,25 +521,27 @@ app.whenReady().then(async () => {
const menu = Menu.getApplicationMenu();
// App menu
- const appMenu = menu.items.find((item) => item.label === 'Goose');
- // add Settings to app menu after About
- appMenu.submenu.insert(1, new MenuItem({ type: 'separator' }));
- appMenu.submenu.insert(
- 1,
- new MenuItem({
- label: 'Settings',
- accelerator: 'CmdOrCtrl+,',
- click() {
- const focusedWindow = BrowserWindow.getFocusedWindow();
- if (focusedWindow) focusedWindow.webContents.send('set-view', 'settings');
- },
- })
- );
- appMenu.submenu.insert(1, new MenuItem({ type: 'separator' }));
+ const appMenu = menu?.items.find((item) => item.label === 'Goose');
+ if (appMenu?.submenu) {
+ // add Settings to app menu after About
+ appMenu.submenu.insert(1, new MenuItem({ type: 'separator' }));
+ appMenu.submenu.insert(
+ 1,
+ new MenuItem({
+ label: 'Settings',
+ accelerator: 'CmdOrCtrl+,',
+ click() {
+ const focusedWindow = BrowserWindow.getFocusedWindow();
+ if (focusedWindow) focusedWindow.webContents.send('set-view', 'settings');
+ },
+ })
+ );
+ appMenu.submenu.insert(1, new MenuItem({ type: 'separator' }));
+ }
// Add Environment menu items to View menu
- const viewMenu = menu.items.find((item) => item.label === 'View');
- if (viewMenu) {
+ const viewMenu = menu?.items.find((item) => item.label === 'View');
+ if (viewMenu?.submenu) {
viewMenu.submenu.append(new MenuItem({ type: 'separator' }));
viewMenu.submenu.append(
new MenuItem({
@@ -549,29 +559,29 @@ app.whenReady().then(async () => {
const fileMenu = menu?.items.find((item) => item.label === 'File');
- // open goose to specific dir and set that as its working space
- fileMenu.submenu.append(
- new MenuItem({
- label: 'Open Directory...',
- accelerator: 'CmdOrCtrl+O',
- click: () => openDirectoryDialog(),
- })
- );
-
- // Add Recent Files submenu
- const recentFilesSubmenu = buildRecentFilesMenu();
- if (recentFilesSubmenu.length > 0) {
- fileMenu.submenu.append(new MenuItem({ type: 'separator' }));
+ if (fileMenu?.submenu) {
+ // open goose to specific dir and set that as its working space
fileMenu.submenu.append(
new MenuItem({
- label: 'Recent Directories',
- submenu: recentFilesSubmenu,
+ label: 'Open Directory...',
+ accelerator: 'CmdOrCtrl+O',
+ click: () => openDirectoryDialog(),
})
);
- }
- // Add menu items to File menu
- if (fileMenu && fileMenu.submenu) {
+ // Add Recent Files submenu
+ const recentFilesSubmenu = buildRecentFilesMenu();
+ if (recentFilesSubmenu.length > 0) {
+ fileMenu.submenu.append(new MenuItem({ type: 'separator' }));
+ fileMenu.submenu.append(
+ new MenuItem({
+ label: 'Recent Directories',
+ submenu: recentFilesSubmenu,
+ })
+ );
+ }
+
+ // Add menu items to File menu
fileMenu.submenu.append(
new MenuItem({
label: 'New Chat Window',
@@ -611,7 +621,9 @@ app.whenReady().then(async () => {
);
}
- Menu.setApplicationMenu(menu);
+ if (menu) {
+ Menu.setApplicationMenu(menu);
+ }
app.on('activate', () => {
if (BrowserWindow.getAllWindows().length === 0) {
@@ -619,7 +631,7 @@ app.whenReady().then(async () => {
}
});
- ipcMain.on('create-chat-window', (_, query, dir, version, resumeSessionId, botConfig) => {
+ ipcMain.on('create-chat-window', (_event, query, dir, version, resumeSessionId, botConfig) => {
if (!dir?.trim()) {
const recentDirs = loadRecentDirs();
dir = recentDirs.length > 0 ? recentDirs[0] : null;
@@ -627,12 +639,12 @@ app.whenReady().then(async () => {
createChat(app, query, dir, version, resumeSessionId, botConfig);
});
- ipcMain.on('notify', (event, data) => {
+ ipcMain.on('notify', (_event, data) => {
console.log('NOTIFY', data);
new Notification({ title: data.title, body: data.body }).show();
});
- ipcMain.on('logInfo', (_, info) => {
+ ipcMain.on('logInfo', (_event, info) => {
log.info('from renderer:', info);
});
@@ -668,12 +680,12 @@ app.whenReady().then(async () => {
});
// Handle binary path requests
- ipcMain.handle('get-binary-path', (event, binaryName) => {
+ ipcMain.handle('get-binary-path', (_event, binaryName) => {
return getBinaryPath(app, binaryName);
});
// Handle metadata fetching from main process
- ipcMain.handle('fetch-metadata', async (_, url) => {
+ ipcMain.handle('fetch-metadata', async (_event, url) => {
try {
const response = await fetch(url, {
headers: {
@@ -692,7 +704,7 @@ app.whenReady().then(async () => {
}
});
- ipcMain.on('open-in-chrome', (_, url) => {
+ ipcMain.on('open-in-chrome', (_event, url) => {
// On macOS, use the 'open' command with Chrome
if (process.platform === 'darwin') {
spawn('open', ['-a', 'Google Chrome', url]);
diff --git a/ui/desktop/src/preload.ts b/ui/desktop/src/preload.ts
index 4809cb78..7980a8a2 100644
--- a/ui/desktop/src/preload.ts
+++ b/ui/desktop/src/preload.ts
@@ -1,11 +1,32 @@
import Electron, { contextBridge, ipcRenderer } from 'electron';
+interface BotConfig {
+ id: string;
+ name: string;
+ description: string;
+ instructions?: string;
+ activities?: string[];
+ [key: string]: unknown;
+}
+
+interface NotificationData {
+ title: string;
+ body: string;
+}
+
+interface FileResponse {
+ file: string;
+ filePath: string;
+ error: string | null;
+ found: boolean;
+}
+
const config = JSON.parse(process.argv.find((arg) => arg.startsWith('{')) || '{}');
// Define the API types in a single place
type ElectronAPI = {
reactReady: () => void;
- getConfig: () => Record;
+ getConfig: () => Record;
hideWindow: () => void;
directoryChooser: (replace?: boolean) => Promise;
createChatWindow: (
@@ -13,36 +34,34 @@ type ElectronAPI = {
dir?: string,
version?: string,
resumeSessionId?: string,
- botConfig?: any
+ botConfig?: BotConfig
) => void;
logInfo: (txt: string) => void;
- showNotification: (data: any) => void;
+ showNotification: (data: NotificationData) => void;
openInChrome: (url: string) => void;
- fetchMetadata: (url: string) => Promise;
+ fetchMetadata: (url: string) => Promise;
reloadApp: () => void;
checkForOllama: () => Promise;
selectFileOrDirectory: () => Promise;
startPowerSaveBlocker: () => Promise;
stopPowerSaveBlocker: () => Promise;
getBinaryPath: (binaryName: string) => Promise;
- readFile: (
- directory: string
- ) => Promise<{ file: string; filePath: string; error: string; found: boolean }>;
+ readFile: (directory: string) => Promise;
writeFile: (directory: string, content: string) => Promise;
on: (
channel: string,
- callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void
+ callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
) => void;
off: (
channel: string,
- callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void
+ callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
) => void;
- emit: (channel: string, ...args: any[]) => void;
+ emit: (channel: string, ...args: unknown[]) => void;
};
type AppConfigAPI = {
- get: (key: string) => any;
- getAll: () => Record;
+ get: (key: string) => unknown;
+ getAll: () => Record;
};
const electronAPI: ElectronAPI = {
@@ -55,10 +74,10 @@ const electronAPI: ElectronAPI = {
dir?: string,
version?: string,
resumeSessionId?: string,
- botConfig?: any
+ botConfig?: BotConfig
) => ipcRenderer.send('create-chat-window', query, dir, version, resumeSessionId, botConfig),
logInfo: (txt: string) => ipcRenderer.send('logInfo', txt),
- showNotification: (data: any) => ipcRenderer.send('notify', data),
+ showNotification: (data: NotificationData) => ipcRenderer.send('notify', data),
openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url),
fetchMetadata: (url: string) => ipcRenderer.invoke('fetch-metadata', url),
reloadApp: () => ipcRenderer.send('reload-app'),
@@ -70,13 +89,19 @@ const electronAPI: ElectronAPI = {
readFile: (filePath: string) => ipcRenderer.invoke('read-file', filePath),
writeFile: (filePath: string, content: string) =>
ipcRenderer.invoke('write-file', filePath, content),
- on: (channel: string, callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
+ on: (
+ channel: string,
+ callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
+ ) => {
ipcRenderer.on(channel, callback);
},
- off: (channel: string, callback: (event: Electron.IpcRendererEvent, ...args: any[]) => void) => {
+ off: (
+ channel: string,
+ callback: (event: Electron.IpcRendererEvent, ...args: unknown[]) => void
+ ) => {
ipcRenderer.off(channel, callback);
},
- emit: (channel: string, ...args: any[]) => {
+ emit: (channel: string, ...args: unknown[]) => {
ipcRenderer.emit(channel, ...args);
},
};
diff --git a/ui/desktop/src/sessionLinks.ts b/ui/desktop/src/sessionLinks.ts
index 9ed3a760..43d72bd9 100644
--- a/ui/desktop/src/sessionLinks.ts
+++ b/ui/desktop/src/sessionLinks.ts
@@ -1,7 +1,14 @@
-import { toast } from 'react-toastify';
import { fetchSharedSessionDetails, SharedSessionDetails } from './sharedSessions';
import { type View } from './App';
+interface SessionLinksViewOptions {
+ sessionDetails?: SharedSessionDetails | null;
+ error?: string;
+ shareToken?: string;
+ baseUrl?: string;
+ [key: string]: unknown;
+}
+
/**
* Handles opening a shared session from a deep link
* @param url The deep link URL (goose://sessions/:shareToken)
@@ -11,7 +18,7 @@ import { type View } from './App';
*/
export async function openSharedSessionFromDeepLink(
url: string,
- setView: (view: View, options?: Record) => void,
+ setView: (view: View, options?: SessionLinksViewOptions) => void,
baseUrl?: string
): Promise {
try {
diff --git a/ui/desktop/src/utils/binaryPath.ts b/ui/desktop/src/utils/binaryPath.ts
index 049e6550..9704a677 100644
--- a/ui/desktop/src/utils/binaryPath.ts
+++ b/ui/desktop/src/utils/binaryPath.ts
@@ -6,7 +6,7 @@ import log from './logger';
export const getBinaryPath = (app: Electron.App, binaryName: string): string => {
const isWindows = process.platform === 'win32';
- const possiblePaths = [];
+ const possiblePaths: string[] = [];
if (isWindows) {
addPaths(isWindows, possiblePaths, `${binaryName}.exe`, app);
addPaths(isWindows, possiblePaths, `${binaryName}.cmd`, app);
@@ -33,7 +33,7 @@ export const getBinaryPath = (app: Electron.App, binaryName: string): string =>
const addPaths = (
isWindows: boolean,
- possiblePaths: any[],
+ possiblePaths: string[],
executableName: string,
app: Electron.App
): void => {
diff --git a/ui/desktop/src/utils/deleteAllKeys.tsx b/ui/desktop/src/utils/deleteAllKeys.tsx
index c95aacb3..3972e9cc 100644
--- a/ui/desktop/src/utils/deleteAllKeys.tsx
+++ b/ui/desktop/src/utils/deleteAllKeys.tsx
@@ -2,7 +2,7 @@ import { getApiUrl, getSecretKey } from '../config';
import { required_keys } from '../components/settings/models/hardcoded_stuff';
export async function DeleteProviderKeysFromKeychain() {
- for (const [provider, keys] of Object.entries(required_keys)) {
+ for (const [_provider, keys] of Object.entries(required_keys)) {
for (const keyName of keys) {
try {
const deleteResponse = await fetch(getApiUrl('/configs/delete'), {
diff --git a/ui/desktop/src/utils/providerUtils.ts b/ui/desktop/src/utils/providerUtils.ts
index aa78c17b..43205864 100644
--- a/ui/desktop/src/utils/providerUtils.ts
+++ b/ui/desktop/src/utils/providerUtils.ts
@@ -16,7 +16,12 @@ import type { ExtensionConfig, FixedExtensionEntry } from '../components/ConfigC
import { toastService } from '../toasts';
import { ExtensionQuery, addExtension as apiAddExtension } from '../api';
-export function getStoredProvider(config: any): string | null {
+interface AppConfig {
+ GOOSE_PROVIDER?: string;
+ [key: string]: unknown;
+}
+
+export function getStoredProvider(config: AppConfig): string | null {
return config.GOOSE_PROVIDER || localStorage.getItem(GOOSE_PROVIDER);
}
diff --git a/ui/desktop/tests/e2e/app.spec.ts b/ui/desktop/tests/e2e/app.spec.ts
index 3463aacf..a7d96684 100644
--- a/ui/desktop/tests/e2e/app.spec.ts
+++ b/ui/desktop/tests/e2e/app.spec.ts
@@ -65,8 +65,8 @@ async function selectProvider(mainWindow: any, provider: Provider) {
await mainWindow.waitForSelector('div[class*="bg-bgSubtle border-b border-borderSubtle"]', { timeout: 5000 });
await mainWindow.waitForTimeout(1000); // Give UI time to stabilize
- const menuButton = await mainWindow.waitForSelector('button:has(svg)', {
- timeout: 5000,
+ const menuButton = await mainWindow.waitForSelector('button[aria-label="More options"]', {
+ timeout: 2000,
state: 'visible'
});
await menuButton.click();
@@ -75,7 +75,7 @@ async function selectProvider(mainWindow: any, provider: Provider) {
await mainWindow.waitForTimeout(1000);
// Click "Reset provider and model"
- const resetProviderButton = await mainWindow.waitForSelector('button:has-text("Reset provider and model")', { timeout: 5000 });
+ const resetProviderButton = await mainWindow.waitForSelector('button:has-text("Reset provider and model")', { timeout: 2000 });
await resetProviderButton.click();
// Wait for page to start refreshing
@@ -90,7 +90,7 @@ async function selectProvider(mainWindow: any, provider: Provider) {
}
// Wait for provider selection screen
- const heading = await mainWindow.waitForSelector('h2:has-text("Choose a Provider")', { timeout: 10000 });
+ const heading = await mainWindow.waitForSelector('h2:has-text("Choose a Provider")', { timeout: 2000 });
const headingText = await heading.textContent();
expect(headingText).toBe('Choose a Provider');
@@ -112,7 +112,7 @@ async function selectProvider(mainWindow: any, provider: Provider) {
// Wait for chat interface to appear
const chatTextarea = await mainWindow.waitForSelector('textarea[placeholder*="What can goose help with?"]',
- { timeout: 15000 });
+ { timeout: 5000 });
expect(await chatTextarea.isVisible()).toBe(true);
// Take screenshot of chat interface
@@ -220,32 +220,47 @@ test.describe('Goose App', () => {
test.describe('General UI', () => {
test('dark mode toggle', async () => {
console.log('Testing dark mode toggle...');
+
+ await selectProvider(mainWindow, providers[0]);
// Click the three dots menu button in the top right
await mainWindow.waitForSelector('div[class*="bg-bgSubtle border-b border-borderSubtle"]');
- const menuButton = await mainWindow.waitForSelector('button:has(svg)', { timeout: 10000 });
+ const menuButton = await mainWindow.waitForSelector('button[aria-label="More options"]', {
+ timeout: 5000,
+ state: 'visible'
+ });
await menuButton.click();
// Find and click the dark mode toggle button
- const darkModeButton = await mainWindow.waitForSelector('button:has-text("Dark Mode"), button:has-text("Light Mode")');
-
+ const darkModeButton = await mainWindow.waitForSelector('button:has-text("Dark")');
+ const lightModeButton = await mainWindow.waitForSelector('button:has-text("Light")');
+ const systemModeButton = await mainWindow.waitForSelector('button:has-text("System")');
+
// Get initial state
const isDarkMode = await mainWindow.evaluate(() => document.documentElement.classList.contains('dark'));
console.log('Initial dark mode state:', isDarkMode);
+
+ if (isDarkMode) {
+ // Click to toggle to light mode
+ await lightModeButton.click();
+ await mainWindow.waitForTimeout(1000);
+ const newDarkMode = await mainWindow.evaluate(() => document.documentElement.classList.contains('dark'));
+ expect(newDarkMode).toBe(!isDarkMode);
+ // Take screenshot to verify and pause to show the change
+ await mainWindow.screenshot({ path: 'test-results/dark-mode-toggle.png' });
+ } else {
+ // Click to toggle to dark mode
+ await darkModeButton.click();
+ await mainWindow.waitForTimeout(1000);
+ const newDarkMode = await mainWindow.evaluate(() => document.documentElement.classList.contains('dark'));
+ expect(newDarkMode).toBe(!isDarkMode);
+ }
+
+ // check that system mode is clickable
+ await systemModeButton.click();
- // Click to toggle
- await darkModeButton.click();
-
- // Verify the change
- const newDarkMode = await mainWindow.evaluate(() => document.documentElement.classList.contains('dark'));
- expect(newDarkMode).toBe(!isDarkMode);
-
- // Take screenshot to verify and pause to show the change
- await mainWindow.screenshot({ path: 'test-results/dark-mode-toggle.png' });
- await mainWindow.waitForTimeout(2000); // Pause in dark/light mode
-
- // Toggle back to original state
- await darkModeButton.click();
+ // Toggle back to light mode
+ await lightModeButton.click();
// Pause to show return to original state
await mainWindow.waitForTimeout(2000);
@@ -331,7 +346,6 @@ test.describe('Goose App', () => {
await chatInput.press('Enter');
// Wait for loading indicator and response
- await mainWindow.waitForSelector('.text-textStandard >> text="goose is working on it…"', { timeout: 10000 });
await mainWindow.waitForSelector('.text-textStandard >> text="goose is working on it…"',
{ state: 'hidden', timeout: 30000 });
@@ -383,12 +397,25 @@ test.describe('Goose App', () => {
await mainWindow.reload();
await mainWindow.waitForLoadState('networkidle');
+ // Debug: Print HTML structure before trying to find menu button
+ // console.log('Debug: Current HTML structure before menu button selection:');
+ // const htmlStructure = await mainWindow.evaluate(() => document.documentElement.outerHTML);
+ // console.log(htmlStructure);
+
+ // Take screenshot before attempting to find menu button
+ await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-before-menu-button.png` });
+
// Click the menu button (3 dots)
- const menuButton = await mainWindow.waitForSelector('button:has(svg)', { timeout: 10000 });
+ console.log('Attempting to find menu button...');
+ const menuButton = await mainWindow.waitForSelector('button[aria-label="More options"]', {
+ timeout: 5000,
+ state: 'visible'
+ });
+ console.log('Menu button found, clicking...');
await menuButton.click();
- // Click Advanced Settings
- const advancedSettingsButton = await mainWindow.waitForSelector('button:has-text("Advanced Settings")');
+ // Click Advanced settings
+ const advancedSettingsButton = await mainWindow.waitForSelector('button:has-text("Advanced settings")');
await advancedSettingsButton.click();
// Wait for settings page and take screenshot
@@ -440,7 +467,7 @@ test.describe('Goose App', () => {
console.log('Extension added successfully');
// Click Exit button to return to chat
- const exitButton = await mainWindow.waitForSelector('button:has-text("Exit")', { timeout: 5000 });
+ const exitButton = await mainWindow.waitForSelector('button:has-text("Back")', { timeout: 5000 });
await exitButton.click();
} catch (error) {
@@ -472,7 +499,7 @@ test.describe('Goose App', () => {
// Wait for loading indicator
const loadingIndicator = await mainWindow.waitForSelector('.text-textStandard >> text="goose is working on it…"',
- { timeout: 10000 });
+ { timeout: 30000 });
expect(await loadingIndicator.isVisible()).toBe(true);
// Take screenshot of loading state
diff --git a/ui/desktop/tsconfig.json b/ui/desktop/tsconfig.json
index 0a9f5aa2..b298d27c 100644
--- a/ui/desktop/tsconfig.json
+++ b/ui/desktop/tsconfig.json
@@ -1,11 +1,45 @@
{
"compilerOptions": {
- "esModuleInterop": true,
+ "target": "ES2020",
+ "useDefineForClassFields": true,
+ "lib": ["ES2020", "DOM", "DOM.Iterable"],
+ "module": "ESNext",
+ "skipLibCheck": true,
+
+ /* Bundler mode */
+ "moduleResolution": "bundler",
"resolveJsonModule": true,
- "jsx": "react",
- "lib": ["dom", "esnext"],
- "paths": {
- "@/*": ["./*"]
- }
- }
+ "isolatedModules": true,
+ "noEmit": true,
+ "jsx": "react-jsx",
+
+ /* Linting */
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noFallthroughCasesInSwitch": true,
+
+ /* Electron/Node.js specific */
+ "types": ["node", "electron", "vite/client"],
+ "typeRoots": ["./node_modules/@types", "./src/types"],
+ "esModuleInterop": true,
+ "allowSyntheticDefaultImports": true,
+ "forceConsistentCasingInFileNames": true,
+
+ /* Source Maps */
+ "sourceMap": true,
+ "inlineSources": true,
+
+ /* Additional Safety */
+ "noImplicitAny": true,
+ "strictNullChecks": true,
+ "strictFunctionTypes": true,
+ "strictBindCallApply": true,
+ "strictPropertyInitialization": true,
+ "noImplicitThis": true,
+ "alwaysStrict": true,
+ "noImplicitReturns": true
+ },
+ "include": ["src"],
+ "references": [{ "path": "./tsconfig.node.json" }]
}
diff --git a/ui/desktop/tsconfig.node.json b/ui/desktop/tsconfig.node.json
new file mode 100644
index 00000000..6503cf99
--- /dev/null
+++ b/ui/desktop/tsconfig.node.json
@@ -0,0 +1,24 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "skipLibCheck": true,
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "allowSyntheticDefaultImports": true,
+ "strict": true,
+ "types": ["node", "electron"],
+ "target": "ES2020",
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "sourceMap": true
+ },
+ "include": [
+ "vite.config.ts",
+ "vite.main.config.ts",
+ "vite.preload.config.ts",
+ "vite.renderer.config.ts",
+ "forge.config.ts",
+ "src/main.ts",
+ "src/preload.ts"
+ ]
+}