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" => { "built-in" => {
let extension = cliclack::select("Which built-in extension would you like to enable?") let extension = cliclack::select("Which built-in extension would you like to enable?")
.item( .item(
"developer", "Developer Tools",
"Developer Tools", "Developer Tools",
"Code editing and shell access", "Code editing and shell access",
) )
.item( .item(
"computercontroller", "Computer Controller",
"Computer Controller", "Computer Controller",
"controls for webscraping, file caching, and automations", "controls for webscraping, file caching, and automations",
) )
.item( .item(
"google_drive", "Google Drive",
"Google Drive", "Google Drive",
"Search and read content from google drive - additional config required", "Search and read content from google drive - additional config required",
) )
.item( .item(
"memory", "Memory",
"Memory", "Memory",
"Tools to save and retrieve durable memories", "Tools to save and retrieve durable memories",
) )
.item( .item(
"tutorial", "Tutorial",
"Tutorial", "Tutorial",
"Access interactive tutorials and guides", "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()) .placeholder(&goose::config::DEFAULT_EXTENSION_TIMEOUT.to_string())
.validate(|input: &String| match input.parse::<u64>() { .validate(|input: &String| match input.parse::<u64>() {
Ok(_) => Ok(()), Ok(_) => Ok(()),
Err(_) => Err("Please enter a valide timeout"), Err(_) => Err("Please enter a valid timeout"),
}) })
.interact()?; .interact()?;

View File

@@ -26,7 +26,6 @@ use goose::providers::base::ProviderMetadata;
super::routes::config_management::ConfigKeyQuery, super::routes::config_management::ConfigKeyQuery,
super::routes::config_management::ConfigResponse, super::routes::config_management::ConfigResponse,
super::routes::config_management::ProvidersResponse, super::routes::config_management::ProvidersResponse,
super::routes::config_management::ProvidersResponse,
super::routes::config_management::ProviderDetails, super::routes::config_management::ProviderDetails,
super::routes::config_management::ExtensionResponse, super::routes::config_management::ExtensionResponse,
super::routes::config_management::ExtensionQuery, super::routes::config_management::ExtensionQuery,

View File

@@ -16,7 +16,10 @@ pub struct ExtensionEntry {
} }
pub fn name_to_key(name: &str) -> String { 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 /// Extension configuration management

View File

@@ -10,7 +10,7 @@
"license": { "license": {
"name": "Apache-2.0" "name": "Apache-2.0"
}, },
"version": "1.0.12" "version": "1.0.13"
}, },
"paths": { "paths": {
"/config": { "/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 { addExtensionFromDeepLink } from './extensions';
import { getStoredModel } from './utils/providerUtils'; import { getStoredModel } from './utils/providerUtils';
import { getStoredProvider, initializeSystem } 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 { useChat } from './hooks/useChat';
import 'react-toastify/dist/ReactToastify.css'; import 'react-toastify/dist/ReactToastify.css';
import { useConfig } from './components/ConfigContext';
import {
initializeBuiltInExtensions,
syncBuiltInExtensions,
} from './components/settings_v2/extensions/LoadBuiltins';
// Views and their options // Views and their options
export type View = export type View =
@@ -57,6 +62,41 @@ export default function App() {
view: 'welcome', view: 'welcome',
viewOptions: {}, 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 [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
const [isLoadingSession, setIsLoadingSession] = useState(false); const [isLoadingSession, setIsLoadingSession] = useState(false);

View File

@@ -1,7 +1,7 @@
[ [
{ {
"id": "developer", "id": "developertools",
"name": "Developer", "name": "Developer Tools",
"description": "General development tools useful for software engineering.", "description": "General development tools useful for software engineering.",
"enabled": true, "enabled": true,
"type": "builtin", "type": "builtin",

View File

@@ -43,7 +43,7 @@ interface ConfigContextType {
toggleExtension: (name: string) => Promise<void>; toggleExtension: (name: string) => Promise<void>;
removeExtension: (name: string) => Promise<void>; removeExtension: (name: string) => Promise<void>;
getProviders: (b: boolean) => Promise<ProviderDetails[]>; getProviders: (b: boolean) => Promise<ProviderDetails[]>;
getExtensions: (b: boolean) => Promise<ExtensionEntry[]> getExtensions: (b: boolean) => Promise<ExtensionEntry[]>;
} }
interface ConfigProviderProps { interface ConfigProviderProps {
@@ -74,12 +74,11 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
// Load extensions // Load extensions
try { try {
const extensionsResponse = await apiGetExtensions() const extensionsResponse = await apiGetExtensions();
setExtensionsList(extensionsResponse.data.extensions) setExtensionsList(extensionsResponse.data.extensions);
} catch (error) { } 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) => { const removeExtension = async (name: string) => {
await apiRemoveExtension({path: {name: name}}); await apiRemoveExtension({ path: { name: name } });
await reloadConfig(); await reloadConfig();
}; };
@@ -133,13 +132,13 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
const query: ExtensionQuery = { name, config, enabled }; const query: ExtensionQuery = { name, config, enabled };
await apiUpdateExtension({ await apiUpdateExtension({
body: query, body: query,
path: {name: name} path: { name: name },
}); });
await reloadConfig(); await reloadConfig();
}; };
const toggleExtension = async (name: string) => { const toggleExtension = async (name: string) => {
await apiToggleExtension({path: {name: name}}); await apiToggleExtension({ path: { name: name } });
await reloadConfig(); await reloadConfig();
}; };
@@ -158,7 +157,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
if (forceRefresh || extensionsList.length === 0) { if (forceRefresh || extensionsList.length === 0) {
// If a refresh is forced, or we don't have providers yet // If a refresh is forced, or we don't have providers yet
const response = await apiGetExtensions(); const response = await apiGetExtensions();
const extensionResponse: ExtensionResponse = response.data const extensionResponse: ExtensionResponse = response.data;
setExtensionsList(extensionResponse.extensions); setExtensionsList(extensionResponse.extensions);
return extensionResponse.extensions; return extensionResponse.extensions;
} }

View File

@@ -74,21 +74,19 @@ export default function ExtensionsSection() {
}); });
useEffect(() => { useEffect(() => {
const extensions = read('extensions', false) const extensions = read('extensions', false);
if (extensions) { if (extensions) {
const extensionItems: ExtensionItem[] = Object.entries(extensions).map( const extensionItems: ExtensionItem[] = Object.entries(extensions).map(([name, ext]) => {
([name, ext]) => { const extensionConfig = ext as ExtensionConfig;
const extensionConfig = ext as ExtensionConfig; return {
return { id: name,
id: name, title: getFriendlyTitle(name),
title: getFriendlyTitle(name), subtitle: getSubtitle(extensionConfig),
subtitle: getSubtitle(extensionConfig), enabled: extensionConfig.enabled,
enabled: extensionConfig.enabled, canConfigure: extensionConfig.type === 'stdio' && !!extensionConfig.envs,
canConfigure: extensionConfig.type === 'stdio' && !!extensionConfig.envs, config: extensionConfig,
config: extensionConfig, };
}; });
}
);
setExtensions(extensionItems); setExtensions(extensionItems);
} }
}, [read]); }, [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);
}