mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-22 00:24:23 +01:00
ui: load builtins (#1679)
This commit is contained in:
@@ -422,27 +422,27 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
|
||||
"built-in" => {
|
||||
let extension = cliclack::select("Which built-in extension would you like to enable?")
|
||||
.item(
|
||||
"developer",
|
||||
"Developer Tools",
|
||||
"Developer Tools",
|
||||
"Code editing and shell access",
|
||||
)
|
||||
.item(
|
||||
"computercontroller",
|
||||
"Computer Controller",
|
||||
"Computer Controller",
|
||||
"controls for webscraping, file caching, and automations",
|
||||
)
|
||||
.item(
|
||||
"google_drive",
|
||||
"Google Drive",
|
||||
"Google Drive",
|
||||
"Search and read content from google drive - additional config required",
|
||||
)
|
||||
.item(
|
||||
"memory",
|
||||
"Memory",
|
||||
"Memory",
|
||||
"Tools to save and retrieve durable memories",
|
||||
)
|
||||
.item(
|
||||
"tutorial",
|
||||
"Tutorial",
|
||||
"Tutorial",
|
||||
"Access interactive tutorials and guides",
|
||||
)
|
||||
@@ -454,7 +454,7 @@ pub fn configure_extensions_dialog() -> Result<(), Box<dyn Error>> {
|
||||
.placeholder(&goose::config::DEFAULT_EXTENSION_TIMEOUT.to_string())
|
||||
.validate(|input: &String| match input.parse::<u64>() {
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err("Please enter a valide timeout"),
|
||||
Err(_) => Err("Please enter a valid timeout"),
|
||||
})
|
||||
.interact()?;
|
||||
|
||||
|
||||
@@ -26,7 +26,6 @@ use goose::providers::base::ProviderMetadata;
|
||||
super::routes::config_management::ConfigKeyQuery,
|
||||
super::routes::config_management::ConfigResponse,
|
||||
super::routes::config_management::ProvidersResponse,
|
||||
super::routes::config_management::ProvidersResponse,
|
||||
super::routes::config_management::ProviderDetails,
|
||||
super::routes::config_management::ExtensionResponse,
|
||||
super::routes::config_management::ExtensionQuery,
|
||||
|
||||
@@ -16,7 +16,10 @@ pub struct ExtensionEntry {
|
||||
}
|
||||
|
||||
pub fn name_to_key(name: &str) -> String {
|
||||
name.to_string()
|
||||
name.chars()
|
||||
.filter(|c| !c.is_whitespace())
|
||||
.collect::<String>()
|
||||
.to_lowercase()
|
||||
}
|
||||
|
||||
/// Extension configuration management
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
"license": {
|
||||
"name": "Apache-2.0"
|
||||
},
|
||||
"version": "1.0.12"
|
||||
"version": "1.0.13"
|
||||
},
|
||||
"paths": {
|
||||
"/config": {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import { addExtensionFromDeepLink } from './extensions';
|
||||
import { getStoredModel } from './utils/providerUtils';
|
||||
import { getStoredProvider, initializeSystem } from './utils/providerUtils';
|
||||
@@ -24,6 +24,11 @@ import ProviderSettings from './components/settings_v2/providers/ProviderSetting
|
||||
import { useChat } from './hooks/useChat';
|
||||
|
||||
import 'react-toastify/dist/ReactToastify.css';
|
||||
import { useConfig } from './components/ConfigContext';
|
||||
import {
|
||||
initializeBuiltInExtensions,
|
||||
syncBuiltInExtensions,
|
||||
} from './components/settings_v2/extensions/LoadBuiltins';
|
||||
|
||||
// Views and their options
|
||||
export type View =
|
||||
@@ -57,6 +62,41 @@ export default function App() {
|
||||
view: 'welcome',
|
||||
viewOptions: {},
|
||||
});
|
||||
const { getExtensions, addExtension } = useConfig();
|
||||
const initAttemptedRef = useRef(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Skip if feature flag is not enabled
|
||||
if (!process.env.ALPHA) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setupExtensions = async () => {
|
||||
try {
|
||||
// Set the ref immediately to prevent duplicate runs
|
||||
initAttemptedRef.current = true;
|
||||
|
||||
// Force refresh extensions from the backend to ensure we have the latest
|
||||
const refreshedExtensions = await getExtensions(true);
|
||||
|
||||
if (refreshedExtensions.length === 0) {
|
||||
// If we still have no extensions, this is truly a first-time setup
|
||||
console.log('First-time setup: Adding all built-in extensions...');
|
||||
await initializeBuiltInExtensions(addExtension);
|
||||
} else {
|
||||
// Extensions exist, check for any missing built-ins
|
||||
console.log('Checking for missing built-in extensions...');
|
||||
console.log(refreshedExtensions);
|
||||
await syncBuiltInExtensions(refreshedExtensions, addExtension);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error setting up extensions:', error);
|
||||
}
|
||||
};
|
||||
|
||||
setupExtensions();
|
||||
}, []); // Empty dependency array since we're using initAttemptedRef
|
||||
|
||||
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
|
||||
const [isLoadingSession, setIsLoadingSession] = useState(false);
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
[
|
||||
{
|
||||
"id": "developer",
|
||||
"name": "Developer",
|
||||
"id": "developertools",
|
||||
"name": "Developer Tools",
|
||||
"description": "General development tools useful for software engineering.",
|
||||
"enabled": true,
|
||||
"type": "builtin",
|
||||
|
||||
@@ -43,7 +43,7 @@ interface ConfigContextType {
|
||||
toggleExtension: (name: string) => Promise<void>;
|
||||
removeExtension: (name: string) => Promise<void>;
|
||||
getProviders: (b: boolean) => Promise<ProviderDetails[]>;
|
||||
getExtensions: (b: boolean) => Promise<ExtensionEntry[]>
|
||||
getExtensions: (b: boolean) => Promise<ExtensionEntry[]>;
|
||||
}
|
||||
|
||||
interface ConfigProviderProps {
|
||||
@@ -74,12 +74,11 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
|
||||
// Load extensions
|
||||
try {
|
||||
const extensionsResponse = await apiGetExtensions()
|
||||
setExtensionsList(extensionsResponse.data.extensions)
|
||||
const extensionsResponse = await apiGetExtensions();
|
||||
setExtensionsList(extensionsResponse.data.extensions);
|
||||
} catch (error) {
|
||||
console.error('Failed to load extensions:', error)
|
||||
console.error('Failed to load extensions:', error);
|
||||
}
|
||||
|
||||
})();
|
||||
}, []);
|
||||
|
||||
@@ -125,7 +124,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
};
|
||||
|
||||
const removeExtension = async (name: string) => {
|
||||
await apiRemoveExtension({path: {name: name}});
|
||||
await apiRemoveExtension({ path: { name: name } });
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
@@ -133,13 +132,13 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
const query: ExtensionQuery = { name, config, enabled };
|
||||
await apiUpdateExtension({
|
||||
body: query,
|
||||
path: {name: name}
|
||||
path: { name: name },
|
||||
});
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
const toggleExtension = async (name: string) => {
|
||||
await apiToggleExtension({path: {name: name}});
|
||||
await apiToggleExtension({ path: { name: name } });
|
||||
await reloadConfig();
|
||||
};
|
||||
|
||||
@@ -158,7 +157,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
if (forceRefresh || extensionsList.length === 0) {
|
||||
// If a refresh is forced, or we don't have providers yet
|
||||
const response = await apiGetExtensions();
|
||||
const extensionResponse: ExtensionResponse = response.data
|
||||
const extensionResponse: ExtensionResponse = response.data;
|
||||
setExtensionsList(extensionResponse.extensions);
|
||||
return extensionResponse.extensions;
|
||||
}
|
||||
|
||||
@@ -74,10 +74,9 @@ export default function ExtensionsSection() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
const extensions = read('extensions', false)
|
||||
const extensions = read('extensions', false);
|
||||
if (extensions) {
|
||||
const extensionItems: ExtensionItem[] = Object.entries(extensions).map(
|
||||
([name, ext]) => {
|
||||
const extensionItems: ExtensionItem[] = Object.entries(extensions).map(([name, ext]) => {
|
||||
const extensionConfig = ext as ExtensionConfig;
|
||||
return {
|
||||
id: name,
|
||||
@@ -87,8 +86,7 @@ export default function ExtensionsSection() {
|
||||
canConfigure: extensionConfig.type === 'stdio' && !!extensionConfig.envs,
|
||||
config: extensionConfig,
|
||||
};
|
||||
}
|
||||
);
|
||||
});
|
||||
setExtensions(extensionItems);
|
||||
}
|
||||
}, [read]);
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
import type { ExtensionConfig } from '../../../api/types.gen';
|
||||
import builtInExtensionsData from '../../../built-in-extensions.json';
|
||||
import { FixedExtensionEntry } from '@/src/components/ConfigContext';
|
||||
|
||||
// Type definition for built-in extensions from JSON
|
||||
type BuiltinExtension = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
enabled: boolean;
|
||||
type: 'builtin';
|
||||
env_keys: string[];
|
||||
timeout?: number;
|
||||
};
|
||||
|
||||
// TODO: need to keep this in sync better with `name_to_key` on the rust side
|
||||
function nameToKey(name: string): string {
|
||||
return name
|
||||
.split('')
|
||||
.filter((char) => !char.match(/\s/))
|
||||
.join('')
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
/**
|
||||
* Synchronizes built-in extensions with the config system.
|
||||
* This function ensures all built-in extensions are added, which is especially
|
||||
* important for first-time users with an empty config.yaml.
|
||||
*
|
||||
* @param existingExtensions Current list of extensions from the config (could be empty)
|
||||
* @param addExtensionFn Function to add a new extension to the config
|
||||
* @returns Promise that resolves when sync is complete
|
||||
*/
|
||||
export async function syncBuiltInExtensions(
|
||||
existingExtensions: FixedExtensionEntry[],
|
||||
addExtensionFn: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>
|
||||
): Promise<void> {
|
||||
try {
|
||||
console.log('Setting up built-in extensions... in syncBuiltinExtensions');
|
||||
|
||||
// Create a set of existing extension IDs for quick lookup
|
||||
const existingExtensionKeys = new Set(existingExtensions.map((ext) => nameToKey(ext.name)));
|
||||
console.log('existing extension ids', existingExtensionKeys);
|
||||
|
||||
// Cast the imported JSON data to the expected type
|
||||
const builtinExtensions = builtInExtensionsData as BuiltinExtension[];
|
||||
|
||||
// Track how many extensions were added
|
||||
let addedCount = 0;
|
||||
|
||||
// Check each built-in extension
|
||||
for (const builtinExt of builtinExtensions) {
|
||||
// Only add if the extension doesn't already exist
|
||||
if (!existingExtensionKeys.has(builtinExt.id)) {
|
||||
console.log(`Adding built-in extension: ${builtinExt.id}`);
|
||||
|
||||
// Convert to the ExtensionConfig format
|
||||
const extConfig: ExtensionConfig = {
|
||||
name: builtinExt.name,
|
||||
type: 'builtin',
|
||||
timeout: builtinExt.timeout ?? 300,
|
||||
};
|
||||
|
||||
// Add the extension with its default enabled state
|
||||
await addExtensionFn(nameToKey(builtinExt.name), extConfig, builtinExt.enabled);
|
||||
addedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (addedCount > 0) {
|
||||
console.log(`Added ${addedCount} built-in extensions.`);
|
||||
} else {
|
||||
console.log('All built-in extensions already present.');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to add built-in extensions:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to initialize all built-in extensions for a first-time user.
|
||||
* This can be called when the application is first installed.
|
||||
*/
|
||||
export async function initializeBuiltInExtensions(
|
||||
addExtensionFn: (name: string, config: ExtensionConfig, enabled: boolean) => Promise<void>
|
||||
): Promise<void> {
|
||||
// Call with an empty list to ensure all built-ins are added
|
||||
await syncBuiltInExtensions([], addExtensionFn);
|
||||
}
|
||||
Reference in New Issue
Block a user