fix: revert the change for unexpected change from ui/goose2 work (#9098)

This commit is contained in:
Lifei Zhou
2026-05-08 19:19:15 +10:00
committed by GitHub
parent c822d1ed82
commit 2db5e58ec7
21 changed files with 207 additions and 201 deletions
+3
View File
@@ -5211,6 +5211,9 @@
},
{
"type": "object",
"required": [
"enabled"
],
"properties": {
"enabled": {
"type": "boolean"
+1 -1
View File
@@ -465,7 +465,7 @@ export type ExtensionData = {
};
export type ExtensionEntry = ExtensionConfig & {
enabled?: boolean;
enabled: boolean;
};
export type ExtensionLoadResult = {
+4 -12
View File
@@ -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: [],
+6 -3
View File
@@ -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."