mirror of
https://github.com/aljazceru/goose.git
synced 2025-12-21 16:14:21 +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" => {
|
"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()?;
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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": {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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]);
|
||||||
|
|||||||
@@ -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