fix: revert the change for unexpected change from ui/goose2 work (#9098)
This commit is contained in:
@@ -465,7 +465,7 @@ export type ExtensionData = {
|
||||
};
|
||||
|
||||
export type ExtensionEntry = ExtensionConfig & {
|
||||
enabled?: boolean;
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
export type ExtensionLoadResult = {
|
||||
|
||||
@@ -18,7 +18,6 @@ import type {
|
||||
ProviderDetails,
|
||||
ExtensionQuery,
|
||||
ExtensionConfig,
|
||||
ExtensionEntry,
|
||||
} from '../api';
|
||||
|
||||
export type { ExtensionConfig } from '../api/types.gen';
|
||||
@@ -28,12 +27,6 @@ export type FixedExtensionEntry = ExtensionConfig & {
|
||||
enabled: boolean;
|
||||
};
|
||||
|
||||
const normalizeExtensions = (extensions: ExtensionEntry[]): FixedExtensionEntry[] =>
|
||||
extensions.map((extension) => ({
|
||||
...extension,
|
||||
enabled: extension.enabled ?? true,
|
||||
}));
|
||||
|
||||
interface ConfigContextType {
|
||||
config: ConfigResponse['config'];
|
||||
providersList: ProviderDetails[];
|
||||
@@ -133,10 +126,9 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
}
|
||||
|
||||
const extensionResponse: ExtensionResponse = result.data!;
|
||||
const extensions = normalizeExtensions(extensionResponse.extensions);
|
||||
setExtensionsList(extensions);
|
||||
setExtensionsList(extensionResponse.extensions);
|
||||
setExtensionWarnings(extensionResponse.warnings || []);
|
||||
return extensions;
|
||||
return extensionResponse.extensions;
|
||||
}, [extensionsList]);
|
||||
|
||||
const addExtension = useCallback(
|
||||
@@ -221,7 +213,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
// Load extensions
|
||||
try {
|
||||
const extensionsResponse = await apiGetExtensions();
|
||||
let extensions = normalizeExtensions(extensionsResponse.data?.extensions || []);
|
||||
let extensions = extensionsResponse.data?.extensions || [];
|
||||
|
||||
// Always sync bundled extensions from bundled-extensions.json
|
||||
// This ensures:
|
||||
@@ -244,7 +236,7 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
|
||||
await syncBundledExtensions(extensions, addExtensionForSync);
|
||||
// Reload extensions after sync
|
||||
const refreshedResponse = await apiGetExtensions();
|
||||
extensions = normalizeExtensions(refreshedResponse.data?.extensions || []);
|
||||
extensions = refreshedResponse.data?.extensions || [];
|
||||
|
||||
setExtensionsList(extensions);
|
||||
setExtensionWarnings(extensionsResponse.data?.warnings || []);
|
||||
|
||||
@@ -32,7 +32,7 @@ const i18n = defineMessages({
|
||||
defaultNote: {
|
||||
id: 'extensionsView.defaultNote',
|
||||
defaultMessage:
|
||||
'Extensions stay available here, and Goose can load them on demand during a chat.',
|
||||
'Extensions enabled here are used as the default for new chats. You can also toggle active extensions during chat.',
|
||||
},
|
||||
addCustomExtension: {
|
||||
id: 'extensionsView.addCustomExtension',
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
getDefaultFormData,
|
||||
} from './utils';
|
||||
|
||||
import { activateExtensionDefault, deleteExtension } from './index';
|
||||
import { activateExtensionDefault, deleteExtension, toggleExtensionDefault } from './index';
|
||||
import { ExtensionConfig } from '../../../api/types.gen';
|
||||
|
||||
const i18n = defineMessages({
|
||||
@@ -44,6 +44,8 @@ interface ExtensionSectionProps {
|
||||
showEnvVars?: boolean;
|
||||
hideButtons?: boolean;
|
||||
disableConfiguration?: boolean;
|
||||
customToggle?: (extension: FixedExtensionEntry) => Promise<boolean | void>;
|
||||
selectedExtensions?: string[]; // Add controlled state
|
||||
onModalClose?: (extensionName: string) => void;
|
||||
searchTerm?: string;
|
||||
}
|
||||
@@ -53,6 +55,8 @@ export default function ExtensionsSection({
|
||||
showEnvVars,
|
||||
hideButtons,
|
||||
disableConfiguration,
|
||||
customToggle,
|
||||
selectedExtensions = [],
|
||||
onModalClose,
|
||||
searchTerm = '',
|
||||
}: ExtensionSectionProps) {
|
||||
@@ -76,26 +80,50 @@ export default function ExtensionsSection({
|
||||
const extensions = useMemo(() => {
|
||||
if (extensionsList.length === 0) return [];
|
||||
|
||||
return [...extensionsList].sort((a, b) => {
|
||||
// First sort by builtin
|
||||
if (a.type === 'builtin' && b.type !== 'builtin') return -1;
|
||||
if (a.type !== 'builtin' && b.type === 'builtin') return 1;
|
||||
return [...extensionsList]
|
||||
.sort((a, b) => {
|
||||
// First sort by builtin
|
||||
if (a.type === 'builtin' && b.type !== 'builtin') return -1;
|
||||
if (a.type !== 'builtin' && b.type === 'builtin') return 1;
|
||||
|
||||
// Then sort by bundled (handle null/undefined cases)
|
||||
const aBundled = 'bundled' in a && a.bundled === true;
|
||||
const bBundled = 'bundled' in b && b.bundled === true;
|
||||
if (aBundled && !bBundled) return -1;
|
||||
if (!aBundled && bBundled) return 1;
|
||||
// Then sort by bundled (handle null/undefined cases)
|
||||
const aBundled = 'bundled' in a && a.bundled === true;
|
||||
const bBundled = 'bundled' in b && b.bundled === true;
|
||||
if (aBundled && !bBundled) return -1;
|
||||
if (!aBundled && bBundled) return 1;
|
||||
|
||||
// Finally sort alphabetically within each group
|
||||
return a.name.localeCompare(b.name);
|
||||
});
|
||||
}, [extensionsList]);
|
||||
// Finally sort alphabetically within each group
|
||||
return a.name.localeCompare(b.name);
|
||||
})
|
||||
.map((ext) => ({
|
||||
...ext,
|
||||
// Use selectedExtensions to determine enabled state in recipe editor
|
||||
enabled: disableConfiguration ? selectedExtensions.includes(ext.name) : ext.enabled,
|
||||
}));
|
||||
}, [extensionsList, disableConfiguration, selectedExtensions]);
|
||||
|
||||
const fetchExtensions = useCallback(async () => {
|
||||
await getExtensions(true); // Force refresh - this will update the context
|
||||
}, [getExtensions]);
|
||||
|
||||
const handleExtensionToggle = async (extensionConfig: FixedExtensionEntry) => {
|
||||
if (customToggle) {
|
||||
await customToggle(extensionConfig);
|
||||
return true;
|
||||
}
|
||||
|
||||
const toggleDirection = extensionConfig.enabled ? 'toggleOff' : 'toggleOn';
|
||||
|
||||
await toggleExtensionDefault({
|
||||
toggle: toggleDirection,
|
||||
extensionConfig: extensionConfig,
|
||||
addToConfig: addExtension,
|
||||
});
|
||||
|
||||
await fetchExtensions();
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleConfigureClick = (extension: FixedExtensionEntry) => {
|
||||
setSelectedExtension(extension);
|
||||
setIsModalOpen(true);
|
||||
@@ -181,6 +209,7 @@ export default function ExtensionsSection({
|
||||
<div className="">
|
||||
<ExtensionList
|
||||
extensions={extensions}
|
||||
onToggle={handleExtensionToggle}
|
||||
onConfigure={handleConfigureClick}
|
||||
disableConfiguration={disableConfiguration}
|
||||
searchTerm={searchTerm}
|
||||
@@ -238,7 +267,7 @@ export default function ExtensionsSection({
|
||||
title={intl.formatMessage(i18n.addCustomExtension)}
|
||||
initialData={extensionToFormData({
|
||||
...deepLinkConfig,
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
} as FixedExtensionEntry)}
|
||||
onClose={handleModalClose}
|
||||
onSubmit={handleAddExtension}
|
||||
|
||||
@@ -9,7 +9,7 @@ vi.mock('./bundled-extensions.json', () => ({
|
||||
name: 'developer',
|
||||
display_name: 'Developer',
|
||||
description: 'General development tools.',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
type: 'builtin',
|
||||
timeout: 300,
|
||||
},
|
||||
@@ -18,7 +18,7 @@ vi.mock('./bundled-extensions.json', () => ({
|
||||
name: 'googledrive',
|
||||
display_name: 'Google Drive',
|
||||
description: 'Google Drive integration.',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
type: 'stdio',
|
||||
cmd: 'googledrive-mcp',
|
||||
args: [],
|
||||
@@ -130,7 +130,7 @@ describe('pruneDeprecatedBundledExtensions', () => {
|
||||
name: 'googledrive',
|
||||
bundled: true,
|
||||
}),
|
||||
false
|
||||
true
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -178,14 +178,15 @@ export async function addExtensionFromDeepLink(
|
||||
return;
|
||||
}
|
||||
|
||||
// Note: deeplink activation doesn't have access to sessionId, so the extension
|
||||
// is saved for on-demand use instead of being globally enabled.
|
||||
await addExtensionFn(config.name, config, false);
|
||||
// Note: deeplink activation doesn't have access to sessionId
|
||||
// The extension will be added to config but not activated in the current session
|
||||
// It will be activated when the next session starts
|
||||
await addExtensionFn(config.name, config, true);
|
||||
|
||||
// Show success toast and navigate to extensions page
|
||||
toastService.success({
|
||||
title: 'Extension Installed',
|
||||
msg: `${config.name} extension has been installed successfully and is available on demand.`,
|
||||
msg: `${config.name} extension has been installed successfully. Start a new chat session to use it.`,
|
||||
});
|
||||
|
||||
// Navigate to extensions page to show the newly installed extension
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
import type { ExtensionConfig } from '../../../api/types.gen';
|
||||
import { toastService } from '../../../toasts';
|
||||
import { trackExtensionAdded, trackExtensionDeleted, getErrorType } from '../../../utils/analytics';
|
||||
import {
|
||||
trackExtensionAdded,
|
||||
trackExtensionEnabled,
|
||||
trackExtensionDisabled,
|
||||
trackExtensionDeleted,
|
||||
getErrorType,
|
||||
} from '../../../utils/analytics';
|
||||
|
||||
function isBuiltinExtension(config: ExtensionConfig): boolean {
|
||||
return config.type === 'builtin';
|
||||
@@ -32,6 +38,46 @@ export async function deleteExtension({
|
||||
}
|
||||
}
|
||||
|
||||
interface ToggleExtensionDefaultProps {
|
||||
toggle: 'toggleOn' | 'toggleOff';
|
||||
extensionConfig: ExtensionConfig;
|
||||
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||
}
|
||||
|
||||
export async function toggleExtensionDefault({
|
||||
toggle,
|
||||
extensionConfig,
|
||||
addToConfig,
|
||||
}: ToggleExtensionDefaultProps) {
|
||||
const isBuiltin = isBuiltinExtension(extensionConfig);
|
||||
const enabled = toggle === 'toggleOn';
|
||||
|
||||
try {
|
||||
await addToConfig(extensionConfig.name, extensionConfig, enabled);
|
||||
if (enabled) {
|
||||
trackExtensionEnabled(extensionConfig.name, true, undefined, isBuiltin);
|
||||
} else {
|
||||
trackExtensionDisabled(extensionConfig.name, true, undefined, isBuiltin);
|
||||
}
|
||||
toastService.success({
|
||||
title: extensionConfig.name,
|
||||
msg: enabled ? 'Extension enabled in defaults' : 'Extension removed from defaults',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to update extension default in config:', error);
|
||||
if (enabled) {
|
||||
trackExtensionEnabled(extensionConfig.name, false, getErrorType(error), isBuiltin);
|
||||
} else {
|
||||
trackExtensionDisabled(extensionConfig.name, false, getErrorType(error), isBuiltin);
|
||||
}
|
||||
toastService.error({
|
||||
title: extensionConfig.name,
|
||||
msg: 'Failed to update extension default',
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
interface ActivateExtensionDefaultProps {
|
||||
addToConfig: (name: string, extensionConfig: ExtensionConfig, enabled: boolean) => Promise<void>;
|
||||
extensionConfig: ExtensionConfig;
|
||||
@@ -44,11 +90,11 @@ export async function activateExtensionDefault({
|
||||
const isBuiltin = isBuiltinExtension(extensionConfig);
|
||||
|
||||
try {
|
||||
await addToConfig(extensionConfig.name, extensionConfig, false);
|
||||
await addToConfig(extensionConfig.name, extensionConfig, true);
|
||||
trackExtensionAdded(extensionConfig.name, true, undefined, isBuiltin);
|
||||
toastService.success({
|
||||
title: extensionConfig.name,
|
||||
msg: 'Extension added',
|
||||
msg: 'Extension added as default',
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Failed to add extension to config:', error);
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
export { DEFAULT_EXTENSION_TIMEOUT, nameToKey } from './utils';
|
||||
|
||||
export { activateExtensionDefault, deleteExtension } from './extension-manager';
|
||||
export {
|
||||
activateExtensionDefault,
|
||||
toggleExtensionDefault,
|
||||
deleteExtension,
|
||||
} from './extension-manager';
|
||||
|
||||
export { pruneDeprecatedBundledExtensions, syncBundledExtensions } from './bundled-extensions';
|
||||
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
import { useState, useEffect } from 'react';
|
||||
import kebabCase from 'lodash/kebabCase';
|
||||
import { Switch } from '../../../ui/switch';
|
||||
import { Gear } from '../../../icons';
|
||||
import { FixedExtensionEntry } from '../../../ConfigContext';
|
||||
import { getSubtitle, getFriendlyTitle } from './ExtensionList';
|
||||
@@ -10,16 +12,59 @@ const i18n = defineMessages({
|
||||
id: 'extensionItem.configureExtension',
|
||||
defaultMessage: 'Configure {name} Extension',
|
||||
},
|
||||
toggleExtension: {
|
||||
id: 'extensionItem.toggleExtension',
|
||||
defaultMessage: 'Toggle {name} extension On or Off',
|
||||
},
|
||||
});
|
||||
|
||||
interface ExtensionItemProps {
|
||||
extension: FixedExtensionEntry;
|
||||
onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void> | void;
|
||||
onConfigure?: (extension: FixedExtensionEntry) => void;
|
||||
isStatic?: boolean; // to not allow users to edit configuration
|
||||
}
|
||||
|
||||
export default function ExtensionItem({ extension, onConfigure, isStatic }: ExtensionItemProps) {
|
||||
export default function ExtensionItem({
|
||||
extension,
|
||||
onToggle,
|
||||
onConfigure,
|
||||
isStatic,
|
||||
}: ExtensionItemProps) {
|
||||
const intl = useIntl();
|
||||
// Add local state to track the visual toggle state
|
||||
const [visuallyEnabled, setVisuallyEnabled] = useState(extension.enabled);
|
||||
// Track if we're in the process of toggling
|
||||
const [isToggling, setIsToggling] = useState(false);
|
||||
|
||||
const handleToggle = async (ext: FixedExtensionEntry) => {
|
||||
// Prevent multiple toggles while one is in progress
|
||||
if (isToggling) return;
|
||||
|
||||
setIsToggling(true);
|
||||
|
||||
// Immediately update visual state
|
||||
const newState = !ext.enabled;
|
||||
setVisuallyEnabled(newState);
|
||||
|
||||
try {
|
||||
// Call the actual toggle function that performs the async operation
|
||||
await onToggle(ext);
|
||||
// Success case is handled by the useEffect below when extension.enabled changes
|
||||
} catch {
|
||||
// If there was an error, revert the visual state
|
||||
setVisuallyEnabled(!newState);
|
||||
} finally {
|
||||
setIsToggling(false);
|
||||
}
|
||||
};
|
||||
|
||||
// Update visual state when the actual extension state changes
|
||||
useEffect(() => {
|
||||
if (!isToggling) {
|
||||
setVisuallyEnabled(extension.enabled);
|
||||
}
|
||||
}, [extension.enabled, isToggling]);
|
||||
|
||||
const renderSubtitle = () => {
|
||||
const { description, command } = getSubtitle(extension);
|
||||
@@ -51,7 +96,6 @@ export default function ExtensionItem({ extension, onConfigure, isStatic }: Exte
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{editable && (
|
||||
<button
|
||||
type="button"
|
||||
className="text-text-secondary hover:text-text-primary"
|
||||
aria-label={intl.formatMessage(i18n.configureExtension, {
|
||||
name: getFriendlyTitle(extension),
|
||||
@@ -61,6 +105,15 @@ export default function ExtensionItem({ extension, onConfigure, isStatic }: Exte
|
||||
<Gear className="w-4 h-4" />
|
||||
</button>
|
||||
)}
|
||||
<Switch
|
||||
checked={(isToggling && visuallyEnabled) || extension.enabled}
|
||||
onCheckedChange={() => handleToggle(extension)}
|
||||
disabled={isToggling}
|
||||
variant="mono"
|
||||
aria-label={intl.formatMessage(i18n.toggleExtension, {
|
||||
name: getFriendlyTitle(extension),
|
||||
})}
|
||||
/>
|
||||
</div>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
|
||||
@@ -8,11 +8,11 @@ import { defineMessages, useIntl } from '../../../../i18n';
|
||||
const i18n = defineMessages({
|
||||
defaultExtensions: {
|
||||
id: 'extensionList.defaultExtensions',
|
||||
defaultMessage: 'Active by Default ({count})',
|
||||
defaultMessage: 'Default Extensions ({count})',
|
||||
},
|
||||
availableExtensions: {
|
||||
id: 'extensionList.availableExtensions',
|
||||
defaultMessage: 'Available On Demand ({count})',
|
||||
defaultMessage: 'Available Extensions ({count})',
|
||||
},
|
||||
noExtensions: {
|
||||
id: 'extensionList.noExtensions',
|
||||
@@ -26,6 +26,7 @@ const i18n = defineMessages({
|
||||
|
||||
interface ExtensionListProps {
|
||||
extensions: FixedExtensionEntry[];
|
||||
onToggle: (extension: FixedExtensionEntry) => Promise<boolean | void> | void;
|
||||
onConfigure?: (extension: FixedExtensionEntry) => void;
|
||||
isStatic?: boolean;
|
||||
disableConfiguration?: boolean;
|
||||
@@ -34,9 +35,10 @@ interface ExtensionListProps {
|
||||
|
||||
export default function ExtensionList({
|
||||
extensions,
|
||||
onToggle,
|
||||
onConfigure,
|
||||
isStatic,
|
||||
disableConfiguration,
|
||||
disableConfiguration: _disableConfiguration,
|
||||
searchTerm = '',
|
||||
}: ExtensionListProps) {
|
||||
const matchesSearch = (extension: FixedExtensionEntry): boolean => {
|
||||
@@ -66,9 +68,6 @@ export default function ExtensionList({
|
||||
const sortedDisabledExtensions = [...disabledExtensions].sort((a, b) =>
|
||||
getFriendlyTitle(a).localeCompare(getFriendlyTitle(b))
|
||||
);
|
||||
const configureHandler = disableConfiguration ? undefined : onConfigure;
|
||||
const hasVisibleExtensions =
|
||||
sortedEnabledExtensions.length > 0 || sortedDisabledExtensions.length > 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
@@ -83,7 +82,8 @@ export default function ExtensionList({
|
||||
<ExtensionItem
|
||||
key={extension.name}
|
||||
extension={extension}
|
||||
onConfigure={configureHandler}
|
||||
onToggle={onToggle}
|
||||
onConfigure={onConfigure}
|
||||
isStatic={isStatic}
|
||||
/>
|
||||
))}
|
||||
@@ -104,7 +104,8 @@ export default function ExtensionList({
|
||||
<ExtensionItem
|
||||
key={extension.name}
|
||||
extension={extension}
|
||||
onConfigure={configureHandler}
|
||||
onToggle={onToggle}
|
||||
onConfigure={onConfigure}
|
||||
isStatic={isStatic}
|
||||
/>
|
||||
))}
|
||||
@@ -112,7 +113,7 @@ export default function ExtensionList({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasVisibleExtensions && (
|
||||
{extensions.length === 0 && (
|
||||
<div className="text-center text-text-secondary py-8">
|
||||
{intl.formatMessage(i18n.noExtensions)}
|
||||
</div>
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('Extension Utils', () => {
|
||||
type: 'stdio',
|
||||
cmd: '',
|
||||
endpoint: '',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
timeout: 300,
|
||||
envVars: [],
|
||||
headers: [],
|
||||
|
||||
@@ -47,7 +47,7 @@ export function getDefaultFormData(): ExtensionFormData {
|
||||
type: 'stdio',
|
||||
cmd: '',
|
||||
endpoint: '',
|
||||
enabled: false,
|
||||
enabled: true,
|
||||
timeout: 300,
|
||||
envVars: [],
|
||||
headers: [],
|
||||
|
||||
@@ -1061,14 +1061,17 @@
|
||||
"extensionItem.configureExtension": {
|
||||
"defaultMessage": "Configure {name} Extension"
|
||||
},
|
||||
"extensionItem.toggleExtension": {
|
||||
"defaultMessage": "Toggle {name} extension On or Off"
|
||||
},
|
||||
"extensionList.availableExtensions": {
|
||||
"defaultMessage": "Available On Demand ({count})"
|
||||
"defaultMessage": "Available Extensions ({count})"
|
||||
},
|
||||
"extensionList.builtInExtension": {
|
||||
"defaultMessage": "Built-in extension"
|
||||
},
|
||||
"extensionList.defaultExtensions": {
|
||||
"defaultMessage": "Active by Default ({count})"
|
||||
"defaultMessage": "Default Extensions ({count})"
|
||||
},
|
||||
"extensionList.noExtensions": {
|
||||
"defaultMessage": "No extensions available"
|
||||
@@ -1128,7 +1131,7 @@
|
||||
"defaultMessage": "Browse extensions"
|
||||
},
|
||||
"extensionsView.defaultNote": {
|
||||
"defaultMessage": "Extensions stay available here, and Goose can load them on demand during a chat."
|
||||
"defaultMessage": "Extensions enabled here are used as the default for new chats. You can also toggle active extensions during chat."
|
||||
},
|
||||
"extensionsView.description": {
|
||||
"defaultMessage": "These extensions use the Model Context Protocol (MCP). They can expand Goose's capabilities using three main components: Prompts, Resources, and Tools. {searchShortcut} to search."
|
||||
|
||||
Reference in New Issue
Block a user