mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-23 17:14:22 +01:00
UI Recipe/Custom Agents (#2119)
Co-authored-by: Kalvin Chau <kalvin@block.xyz>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState, useCallback } from 'react';
|
||||
import { IpcRendererEvent } from 'electron';
|
||||
import { addExtensionFromDeepLink } from './extensions';
|
||||
import { openSharedSessionFromDeepLink } from './sessionLinks';
|
||||
@@ -27,7 +27,9 @@ import ConfigureProvidersView from './components/settings/providers/ConfigurePro
|
||||
import SessionsView from './components/sessions/SessionsView';
|
||||
import SharedSessionView from './components/sessions/SharedSessionView';
|
||||
import ProviderSettings from './components/settings_v2/providers/ProviderSettingsPage';
|
||||
import RecipeEditor from './components/RecipeEditor';
|
||||
import { useChat } from './hooks/useChat';
|
||||
import { addExtension as addExtensionDirect, FullExtensionConfig } from './extensions';
|
||||
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useConfig, MalformedConfigError } from './components/ConfigContext';
|
||||
@@ -46,7 +48,8 @@ export type View =
|
||||
| 'settingsV2'
|
||||
| 'sessions'
|
||||
| 'sharedSession'
|
||||
| 'loading';
|
||||
| 'loading'
|
||||
| 'recipeEditor';
|
||||
|
||||
export type ViewOptions =
|
||||
| SettingsViewOptions
|
||||
@@ -59,16 +62,47 @@ export type ViewConfig = {
|
||||
viewOptions?: ViewOptions;
|
||||
};
|
||||
|
||||
const getInitialView = (): ViewConfig => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const viewFromUrl = urlParams.get('view');
|
||||
const windowConfig = window.electron.getConfig();
|
||||
|
||||
if (viewFromUrl === 'recipeEditor' && windowConfig?.recipeConfig) {
|
||||
return {
|
||||
view: 'recipeEditor',
|
||||
viewOptions: {
|
||||
config: windowConfig.recipeConfig,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Any other URL-specified view
|
||||
if (viewFromUrl) {
|
||||
return {
|
||||
view: viewFromUrl as View,
|
||||
viewOptions: {},
|
||||
};
|
||||
}
|
||||
|
||||
// Default case
|
||||
return {
|
||||
view: 'welcome',
|
||||
viewOptions: {},
|
||||
};
|
||||
};
|
||||
|
||||
export default function App() {
|
||||
const [fatalError, setFatalError] = useState<string | null>(null);
|
||||
const [modalVisible, setModalVisible] = useState(false);
|
||||
const [pendingLink, setPendingLink] = useState<string | null>(null);
|
||||
const [modalMessage, setModalMessage] = useState<string>('');
|
||||
const [{ view, viewOptions }, setInternalView] = useState<ViewConfig>({
|
||||
view: 'loading',
|
||||
viewOptions: {},
|
||||
});
|
||||
const { getExtensions, addExtension, read } = useConfig();
|
||||
const [{ view, viewOptions }, setInternalView] = useState<ViewConfig>(getInitialView());
|
||||
const {
|
||||
getExtensions,
|
||||
addExtension: addExtensionToConfig,
|
||||
disableAllExtensions,
|
||||
read,
|
||||
} = useConfig();
|
||||
const initAttemptedRef = useRef(false);
|
||||
|
||||
// Utility function to extract the command from the link
|
||||
@@ -90,7 +124,153 @@ export default function App() {
|
||||
setInternalView({ view, viewOptions });
|
||||
};
|
||||
|
||||
// Single initialization effect that handles both v1 and v2 settings
|
||||
const disableAllStoredExtensions = () => {
|
||||
const userSettingsStr = localStorage.getItem('user_settings');
|
||||
if (!userSettingsStr) return;
|
||||
|
||||
try {
|
||||
const userSettings = JSON.parse(userSettingsStr);
|
||||
// Store original state before modifying
|
||||
localStorage.setItem('user_settings_backup', userSettingsStr);
|
||||
console.log('Backing up user_settings');
|
||||
|
||||
// Disable all extensions
|
||||
userSettings.extensions = userSettings.extensions.map((ext) => ({
|
||||
...ext,
|
||||
enabled: false,
|
||||
}));
|
||||
|
||||
localStorage.setItem('user_settings', JSON.stringify(userSettings));
|
||||
console.log('Disabled all stored extensions');
|
||||
window.electron.emit('settings-updated');
|
||||
} catch (error) {
|
||||
console.error('Error disabling stored extensions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Function to restore original extension states for new non-recipe windows
|
||||
const restoreOriginalExtensionStates = () => {
|
||||
const backupStr = localStorage.getItem('user_settings_backup');
|
||||
if (backupStr) {
|
||||
localStorage.setItem('user_settings', backupStr);
|
||||
console.log('Restored original extension states');
|
||||
}
|
||||
};
|
||||
|
||||
const updateUserSettingsWithConfig = (extensions: FullExtensionConfig[]) => {
|
||||
try {
|
||||
const userSettingsStr = localStorage.getItem('user_settings');
|
||||
const userSettings = userSettingsStr ? JSON.parse(userSettingsStr) : { extensions: [] };
|
||||
|
||||
// For each extension in the passed in config
|
||||
extensions.forEach((newExtension) => {
|
||||
// Find if this extension already exists
|
||||
const existingIndex = userSettings.extensions.findIndex(
|
||||
(ext) => ext.id === newExtension.id
|
||||
);
|
||||
|
||||
if (existingIndex !== -1) {
|
||||
// Extension exists - just set its enabled to true
|
||||
userSettings.extensions[existingIndex].enabled = true;
|
||||
} else {
|
||||
// Extension is new - add it to the array
|
||||
userSettings.extensions.push({
|
||||
...newExtension,
|
||||
enabled: true,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
localStorage.setItem('user_settings', JSON.stringify(userSettings));
|
||||
console.log('Updated user settings with new/enabled extensions:', userSettings.extensions);
|
||||
|
||||
// Notify any listeners (like the settings page) that settings have changed
|
||||
window.electron.emit('settings-updated');
|
||||
} catch (error) {
|
||||
console.error('Error updating user settings:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const enableRecipeConfigExtensions = async (extensions: FullExtensionConfig[]) => {
|
||||
if (!extensions?.length) {
|
||||
console.log('No extensions to enable from bot config');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Enabling ${extensions.length} extensions from bot config:`, extensions);
|
||||
|
||||
disableAllStoredExtensions();
|
||||
|
||||
// Wait for initial server readiness
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
|
||||
for (const extension of extensions) {
|
||||
try {
|
||||
console.log(`Enabling extension: ${extension.name}`);
|
||||
const extensionConfig = {
|
||||
...extension,
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
// Try to add the extension
|
||||
const response = await addExtensionDirect(extensionConfig, false);
|
||||
|
||||
if (!response.ok) {
|
||||
console.error(
|
||||
`Failed to enable extension ${extension.name}: Server returned ${response.status}`
|
||||
);
|
||||
// If it's a 428, retry once
|
||||
if (response.status === 428) {
|
||||
console.log('Server not ready, waiting and will retry...');
|
||||
await new Promise((resolve) => setTimeout(resolve, 2000));
|
||||
try {
|
||||
await addExtensionDirect(extensionConfig, true);
|
||||
console.log(`Successfully enabled extension ${extension.name} on retry`);
|
||||
} catch (retryError) {
|
||||
console.error(`Failed to enable extension ${extension.name} on retry:`, retryError);
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
updateUserSettingsWithConfig(extensions);
|
||||
|
||||
console.log(`Successfully enabled extension: ${extension.name}`);
|
||||
} catch (error) {
|
||||
console.error(`Failed to enable extension ${extension.name}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Finished enabling bot config extensions');
|
||||
};
|
||||
|
||||
const enableRecipeConfigExtensionsV2 = useCallback(
|
||||
async (extensions: FullExtensionConfig[]) => {
|
||||
if (!extensions?.length) {
|
||||
console.log('No extensions to enable from bot config');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await disableAllExtensions();
|
||||
console.log('Disabled all existing extensions');
|
||||
|
||||
for (const extension of extensions) {
|
||||
try {
|
||||
console.log('Enabling extension: ${extension.name}');
|
||||
await addExtensionToConfig(extension.name, extension, true);
|
||||
} catch (error) {
|
||||
console.error(`Failed to enable extension ${extension.name}:`, error);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to enable bot extensions');
|
||||
}
|
||||
console.log('Finished enabling bot config extensions');
|
||||
},
|
||||
[disableAllExtensions, addExtensionToConfig]
|
||||
);
|
||||
|
||||
// settings v2 initialization
|
||||
useEffect(() => {
|
||||
if (!settingsV2Enabled) {
|
||||
return;
|
||||
@@ -105,6 +285,27 @@ export default function App() {
|
||||
|
||||
console.log(`Initializing app with settings v2`);
|
||||
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const viewType = urlParams.get('view');
|
||||
const recipeConfig = window.appConfig.get('recipeConfig');
|
||||
|
||||
// Handle bot config extensions first
|
||||
if (recipeConfig?.extensions?.length > 0 && viewType != 'recipeEditor') {
|
||||
console.log('Found extensions in bot config:', recipeConfig.extensions);
|
||||
enableRecipeConfigExtensionsV2(recipeConfig.extensions);
|
||||
}
|
||||
|
||||
// If we have a specific view type in the URL, use that and skip provider detection
|
||||
if (viewType) {
|
||||
if (viewType === 'recipeEditor' && recipeConfig) {
|
||||
console.log('Setting view to recipeEditor with config:', recipeConfig);
|
||||
setView('recipeEditor', { config: recipeConfig });
|
||||
} else {
|
||||
setView(viewType as View);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const initializeApp = async () => {
|
||||
try {
|
||||
// Initialize config first
|
||||
@@ -121,7 +322,7 @@ export default function App() {
|
||||
try {
|
||||
await initializeSystem(provider, model, {
|
||||
getExtensions,
|
||||
addExtension,
|
||||
addExtensionToConfig,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error in initialization:', error);
|
||||
@@ -153,7 +354,7 @@ export default function App() {
|
||||
console.error('Unhandled error in initialization:', error);
|
||||
setFatalError(`${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
});
|
||||
}, [read, getExtensions, addExtension]);
|
||||
}, [read, getExtensions, addExtensionToConfig, enableRecipeConfigExtensionsV2]);
|
||||
|
||||
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
|
||||
const [isLoadingSession, setIsLoadingSession] = useState(false);
|
||||
@@ -243,6 +444,24 @@ export default function App() {
|
||||
setView(newView);
|
||||
};
|
||||
|
||||
// Get initial view and config
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const viewFromUrl = urlParams.get('view');
|
||||
if (viewFromUrl) {
|
||||
// Get the config from the electron window config
|
||||
const windowConfig = window.electron.getConfig();
|
||||
|
||||
if (viewFromUrl === 'recipeEditor') {
|
||||
const initialViewOptions = {
|
||||
recipeConfig: windowConfig?.recipeConfig,
|
||||
view: viewFromUrl,
|
||||
};
|
||||
setView(viewFromUrl, initialViewOptions);
|
||||
} else {
|
||||
setView(viewFromUrl);
|
||||
}
|
||||
}
|
||||
|
||||
window.electron.on('set-view', handleSetView);
|
||||
return () => window.electron.off('set-view', handleSetView);
|
||||
}, []);
|
||||
@@ -250,7 +469,7 @@ export default function App() {
|
||||
// Add cleanup for session states when view changes
|
||||
useEffect(() => {
|
||||
console.log(`View changed to: ${view}`);
|
||||
if (view !== 'chat') {
|
||||
if (view !== 'chat' && view !== 'recipeEditor') {
|
||||
console.log('Not in chat view, clearing loading session state');
|
||||
setIsLoadingSession(false);
|
||||
}
|
||||
@@ -291,7 +510,7 @@ export default function App() {
|
||||
setModalVisible(false); // Dismiss modal immediately
|
||||
try {
|
||||
if (settingsV2Enabled) {
|
||||
await addExtensionFromDeepLinkV2(pendingLink, addExtension, setView);
|
||||
await addExtensionFromDeepLinkV2(pendingLink, addExtensionToConfig, setView);
|
||||
} else {
|
||||
await addExtensionFromDeepLink(pendingLink, setView);
|
||||
}
|
||||
@@ -318,12 +537,40 @@ export default function App() {
|
||||
const { addRecentModel } = useRecentModels(); // TODO: remove
|
||||
|
||||
useEffect(() => {
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const viewType = urlParams.get('view');
|
||||
const recipeConfig = window.appConfig.get('recipeConfig');
|
||||
|
||||
if (settingsV2Enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Initializing app with settings v1`);
|
||||
|
||||
// Handle bot config extensions first
|
||||
if (recipeConfig?.extensions?.length > 0 && viewType != 'recipeEditor') {
|
||||
console.log('Found extensions in bot config:', recipeConfig.extensions);
|
||||
enableRecipeConfigExtensions(recipeConfig.extensions);
|
||||
}
|
||||
|
||||
// If we have a specific view type in the URL, use that and skip provider detection
|
||||
if (viewType) {
|
||||
if (viewType === 'recipeEditor' && recipeConfig) {
|
||||
console.log('Setting view to recipeEditor with config:', recipeConfig);
|
||||
setView('recipeEditor', { config: recipeConfig });
|
||||
} else {
|
||||
setView(viewType as View);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// if not in any of the states above (in a regular chat)
|
||||
if (!recipeConfig) {
|
||||
restoreOriginalExtensionStates();
|
||||
}
|
||||
|
||||
console.log(`Initializing app with settings v1`);
|
||||
|
||||
// Attempt to detect config for a stored provider
|
||||
const detectStoredProvider = () => {
|
||||
try {
|
||||
@@ -510,6 +757,27 @@ export default function App() {
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{view === 'recipeEditor' && (
|
||||
<RecipeEditor
|
||||
key={viewOptions?.config ? 'with-config' : 'no-config'}
|
||||
config={viewOptions?.config || window.electron.getConfig().recipeConfig}
|
||||
onClose={() => setView('chat')}
|
||||
setView={setView}
|
||||
onSave={(config) => {
|
||||
console.log('Saving recipe config:', config);
|
||||
window.electron.createChatWindow(
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
undefined,
|
||||
config,
|
||||
'recipeEditor',
|
||||
{ config }
|
||||
);
|
||||
setView('chat');
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{isGoosehintsModalOpen && (
|
||||
|
||||
Reference in New Issue
Block a user