ui: load builtins (#1679)

This commit is contained in:
Lily Delalande
2025-03-13 19:40:39 -07:00
committed by GitHub
parent bc0158c828
commit 49dc396c83
9 changed files with 164 additions and 35 deletions

View File

@@ -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()?;

View File

@@ -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,

View File

@@ -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

View File

@@ -10,7 +10,7 @@
"license": {
"name": "Apache-2.0"
},
"version": "1.0.12"
"version": "1.0.13"
},
"paths": {
"/config": {

View File

@@ -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);

View File

@@ -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",

View File

@@ -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);
}
})();
}, []);
@@ -133,7 +132,7 @@ 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();
};
@@ -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;
}

View File

@@ -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]);

View File

@@ -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);
}