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