UI update with sidebar and settings tabs (#3288)
Co-authored-by: Nahiyan Khan <nahiyan@squareup.com> Co-authored-by: Taylor Ho <taylorkmho@gmail.com> Co-authored-by: Lily Delalande <119957291+lily-de@users.noreply.github.com> Co-authored-by: Spence <spencrmartin@gmail.com> Co-authored-by: spencrmartin <spencermartin@squareup.com> Co-authored-by: Judson Stephenson <Jud@users.noreply.github.com> Co-authored-by: Max Novich <mnovich@squareup.com> Co-authored-by: Best Codes <106822363+The-Best-Codes@users.noreply.github.com> Co-authored-by: caroline-a-mckenzie <cmckenzie@squareup.com> Co-authored-by: Michael Neale <michael.neale@gmail.com>
This commit is contained in:
@@ -133,8 +133,10 @@ export default function ExtensionsSection({
|
||||
console.error('Failed to activate extension:', error);
|
||||
// Even if activation fails, we don't reopen the modal
|
||||
} finally {
|
||||
// Refresh the extensions list regardless of success or failure
|
||||
await fetchExtensions();
|
||||
// Add a small delay to ensure backend has updated, then refresh the extensions list
|
||||
setTimeout(async () => {
|
||||
await fetchExtensions();
|
||||
}, 500);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -183,18 +185,8 @@ export default function ExtensionsSection({
|
||||
};
|
||||
|
||||
return (
|
||||
<section id="extensions" className="px-8">
|
||||
<div className="flex justify-between items-center mb-2">
|
||||
<h2 className="text-xl font-medium text-textStandard">Extensions</h2>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-sm text-textStandard mb-6">
|
||||
These extensions use the Model Context Protocol (MCP). They can expand Goose's
|
||||
capabilities using three main components: Prompts, Resources, and Tools.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border-b border-borderSubtle pb-8">
|
||||
<section id="extensions">
|
||||
<div className="">
|
||||
<ExtensionList
|
||||
extensions={extensions}
|
||||
onToggle={handleExtensionToggle}
|
||||
@@ -205,14 +197,16 @@ export default function ExtensionsSection({
|
||||
{!hideButtons && (
|
||||
<div className="flex gap-4 pt-4 w-full">
|
||||
<Button
|
||||
className="flex items-center gap-2 justify-center text-white dark:text-black bg-bgAppInverse hover:bg-bgStandardInverse [&>svg]:!size-4"
|
||||
className="flex items-center gap-2 justify-center"
|
||||
variant="default"
|
||||
onClick={() => setIsAddModalOpen(true)}
|
||||
>
|
||||
<Plus className="h-4 w-4" />
|
||||
Add custom extension
|
||||
</Button>
|
||||
<Button
|
||||
className="flex items-center gap-2 justify-center text-textStandard bg-bgApp border border-borderSubtle hover:border-borderProminent hover:bg-bgApp [&>svg]:!size-4"
|
||||
className="flex items-center gap-2 justify-center"
|
||||
variant="secondary"
|
||||
onClick={() => window.open('https://block.github.io/goose/v1/extensions/', '_blank')}
|
||||
>
|
||||
<GPSIcon size={12} />
|
||||
|
||||
@@ -109,6 +109,9 @@ export async function addExtensionFromDeepLink(
|
||||
| { deepLinkConfig: ExtensionConfig; showEnvVars: boolean }
|
||||
) => void
|
||||
) {
|
||||
console.log('=== addExtensionFromDeepLink Debug ===');
|
||||
console.log('URL:', url);
|
||||
|
||||
const parsedUrl = new URL(url);
|
||||
|
||||
if (parsedUrl.protocol !== 'goose:') {
|
||||
@@ -151,12 +154,14 @@ export async function addExtensionFromDeepLink(
|
||||
// Check if extension requires env vars and go to settings if so
|
||||
if (config.envs && Object.keys(config.envs).length > 0) {
|
||||
console.log('Environment variables required, redirecting to settings');
|
||||
console.log('Calling setView with:', { deepLinkConfig: config, showEnvVars: true });
|
||||
setView('settings', { deepLinkConfig: config, showEnvVars: true });
|
||||
return;
|
||||
}
|
||||
|
||||
// If no env vars are required, proceed with adding the extension
|
||||
try {
|
||||
console.log('No env vars required, activating extension directly');
|
||||
await activateExtension({ extensionConfig: config, addToConfig: addExtensionFn });
|
||||
} catch (error) {
|
||||
console.error('Failed to activate extension from deeplink:', error);
|
||||
|
||||
@@ -188,7 +188,7 @@ export default function EnvVarsSection({
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
variant="ghost"
|
||||
className="flex items-center justify-start gap-1 px-2 pr-4 text-sm rounded-full text-textStandard bg-bgApp border border-borderSubtle hover:border-borderStandard transition-colors min-w-[60px] h-9 [&>svg]:!size-4"
|
||||
className="flex items-center justify-start gap-1 px-2 pr-4 text-sm rounded-full text-textStandard bg-background-default border border-borderSubtle hover:border-borderStandard transition-colors min-w-[60px] h-9 [&>svg]:!size-4"
|
||||
>
|
||||
<Plus /> Add
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { useState } from 'react';
|
||||
import { Button } from '../../../ui/button';
|
||||
import Modal from '../../../Modal';
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
} from '../../../ui/dialog';
|
||||
import { ExtensionFormData } from '../utils';
|
||||
import EnvVarsSection from './EnvVarsSection';
|
||||
import HeadersSection from './HeadersSection';
|
||||
@@ -80,7 +87,7 @@ export default function ExtensionModal({
|
||||
};
|
||||
|
||||
// Handle backdrop close with confirmation if needed
|
||||
const handleBackdropClose = () => {
|
||||
const handleClose = () => {
|
||||
if (hasFormChanges()) {
|
||||
setShowCloseConfirmation(true);
|
||||
} else {
|
||||
@@ -271,150 +278,142 @@ export default function ExtensionModal({
|
||||
}
|
||||
};
|
||||
|
||||
// Create footer buttons based on current state
|
||||
const footerContent = showDeleteConfirmation ? (
|
||||
// Delete confirmation footer
|
||||
<>
|
||||
<div className="w-full px-6 py-4 bg-red-900/20 border-t border-red-500/30">
|
||||
<p className="text-red-400 text-sm mb-2">
|
||||
Are you sure you want to remove "{formData.name}"? This action cannot be undone.
|
||||
</p>
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (onDelete) {
|
||||
onDelete(formData.name);
|
||||
onClose(); // Add this line to close the modal after deletion
|
||||
}
|
||||
}}
|
||||
className="w-full h-[60px] rounded-none border-b border-borderSubtle bg-transparent hover:bg-red-900/20 text-red-500 font-medium text-md"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" /> Confirm removal
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => setShowDeleteConfirmation(false)}
|
||||
variant="ghost"
|
||||
className="w-full h-[60px] rounded-none hover:bg-bgSubtle text-textSubtle hover:text-textStandard text-md font-regular"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
// Normal footer
|
||||
<>
|
||||
{modalType === 'edit' && onDelete && (
|
||||
<Button
|
||||
onClick={() => setShowDeleteConfirmation(true)}
|
||||
className="w-full h-[60px] rounded-none border-b border-borderSubtle bg-transparent hover:bg-bgSubtle text-red-500 font-medium text-md [&>svg]:!size-4"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" /> Remove extension
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
className="w-full h-[60px] rounded-none border-b border-borderSubtle bg-transparent hover:bg-bgSubtle text-textProminent font-medium text-md"
|
||||
>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleBackdropClose}
|
||||
variant="ghost"
|
||||
className="w-full h-[60px] rounded-none hover:bg-bgSubtle text-textSubtle hover:text-textStandard text-md font-regular"
|
||||
>
|
||||
Cancel
|
||||
</Button>
|
||||
</>
|
||||
);
|
||||
|
||||
// Update title based on current state
|
||||
const modalTitle = showDeleteConfirmation ? `Delete Extension "${formData.name}"` : title;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Modal footer={footerContent} onClose={handleBackdropClose}>
|
||||
{/* Title and Icon */}
|
||||
<div className="flex flex-col mb-6">
|
||||
<div>{getModalIcon()}</div>
|
||||
<div className="mt-2">
|
||||
<h2 className="text-2xl font-regular text-textStandard">{modalTitle}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<Dialog open={true} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[600px] max-h-[90vh] overflow-y-auto">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
{getModalIcon()}
|
||||
{modalTitle}
|
||||
</DialogTitle>
|
||||
{showDeleteConfirmation && (
|
||||
<DialogDescription>
|
||||
This will permanently remove this extension and all of its settings.
|
||||
</DialogDescription>
|
||||
)}
|
||||
</DialogHeader>
|
||||
|
||||
{showDeleteConfirmation ? (
|
||||
<div className="mb-6">
|
||||
<p className="text-textStandard">
|
||||
This will permanently remove this extension and all of its settings.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Form Fields */}
|
||||
{/* Name and Type */}
|
||||
<ExtensionInfoFields
|
||||
name={formData.name}
|
||||
type={formData.type}
|
||||
description={formData.description}
|
||||
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
|
||||
submitAttempted={submitAttempted}
|
||||
/>
|
||||
|
||||
{/* Divider */}
|
||||
<hr className="border-t border-borderSubtle mb-4" />
|
||||
|
||||
{/* Command */}
|
||||
<div className="mb-6">
|
||||
<ExtensionConfigFields
|
||||
{showDeleteConfirmation ? (
|
||||
<div className="py-4">
|
||||
<p className="text-textStandard">
|
||||
This will permanently remove this extension and all of its settings.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-4 space-y-6">
|
||||
{/* Form Fields */}
|
||||
{/* Name and Type */}
|
||||
<ExtensionInfoFields
|
||||
name={formData.name}
|
||||
type={formData.type}
|
||||
full_cmd={formData.cmd || ''}
|
||||
endpoint={formData.endpoint || ''}
|
||||
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
|
||||
submitAttempted={submitAttempted}
|
||||
isValid={isConfigValid()}
|
||||
/>
|
||||
<div className="mb-4" />
|
||||
<ExtensionTimeoutField
|
||||
timeout={formData.timeout || 300}
|
||||
description={formData.description}
|
||||
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
|
||||
submitAttempted={submitAttempted}
|
||||
/>
|
||||
|
||||
{/* Divider */}
|
||||
<hr className="border-t border-borderSubtle" />
|
||||
|
||||
{/* Command */}
|
||||
<div>
|
||||
<ExtensionConfigFields
|
||||
type={formData.type}
|
||||
full_cmd={formData.cmd || ''}
|
||||
endpoint={formData.endpoint || ''}
|
||||
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
|
||||
submitAttempted={submitAttempted}
|
||||
isValid={isConfigValid()}
|
||||
/>
|
||||
<div className="mb-4" />
|
||||
<ExtensionTimeoutField
|
||||
timeout={formData.timeout || 300}
|
||||
onChange={(key, value) => setFormData({ ...formData, [key]: value })}
|
||||
submitAttempted={submitAttempted}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Divider */}
|
||||
<hr className="border-t border-borderSubtle" />
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div>
|
||||
<EnvVarsSection
|
||||
envVars={formData.envVars}
|
||||
onAdd={handleAddEnvVar}
|
||||
onRemove={handleRemoveEnvVar}
|
||||
onChange={handleEnvVarChange}
|
||||
submitAttempted={submitAttempted}
|
||||
onPendingInputChange={setHasPendingEnvVars}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Divider */}
|
||||
<hr className="border-t border-borderSubtle mb-4" />
|
||||
{/* Request Headers - Only for streamable_http */}
|
||||
{formData.type === 'streamable_http' && (
|
||||
<>
|
||||
{/* Divider */}
|
||||
<hr className="border-t border-borderSubtle mb-4" />
|
||||
|
||||
{/* Environment Variables */}
|
||||
<div className="mb-6">
|
||||
<EnvVarsSection
|
||||
envVars={formData.envVars}
|
||||
onAdd={handleAddEnvVar}
|
||||
onRemove={handleRemoveEnvVar}
|
||||
onChange={handleEnvVarChange}
|
||||
submitAttempted={submitAttempted}
|
||||
onPendingInputChange={setHasPendingEnvVars}
|
||||
/>
|
||||
</div>
|
||||
<div className="mb-6">
|
||||
<HeadersSection
|
||||
headers={formData.headers}
|
||||
onAdd={handleAddHeader}
|
||||
onRemove={handleRemoveHeader}
|
||||
onChange={handleHeaderChange}
|
||||
submitAttempted={submitAttempted}
|
||||
onPendingInputChange={setHasPendingHeaders}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Request Headers - Only for streamable_http */}
|
||||
{formData.type === 'streamable_http' && (
|
||||
<DialogFooter className="pt-2">
|
||||
{showDeleteConfirmation ? (
|
||||
<>
|
||||
{/* Divider */}
|
||||
<hr className="border-t border-borderSubtle mb-4" />
|
||||
|
||||
<div className="mb-6">
|
||||
<HeadersSection
|
||||
headers={formData.headers}
|
||||
onAdd={handleAddHeader}
|
||||
onRemove={handleRemoveHeader}
|
||||
onChange={handleHeaderChange}
|
||||
submitAttempted={submitAttempted}
|
||||
onPendingInputChange={setHasPendingHeaders}
|
||||
/>
|
||||
</div>
|
||||
<Button variant="outline" onClick={() => setShowDeleteConfirmation(false)}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button
|
||||
onClick={() => {
|
||||
if (onDelete) {
|
||||
onDelete(formData.name);
|
||||
onClose();
|
||||
}
|
||||
}}
|
||||
variant="destructive"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Confirm removal
|
||||
</Button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
{modalType === 'edit' && onDelete && (
|
||||
<Button
|
||||
onClick={() => setShowDeleteConfirmation(true)}
|
||||
variant="outline"
|
||||
className="text-red-500 hover:text-red-600"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 mr-2" />
|
||||
Remove extension
|
||||
</Button>
|
||||
)}
|
||||
<Button variant="outline" onClick={handleClose}>
|
||||
Cancel
|
||||
</Button>
|
||||
<Button onClick={handleSubmit} disabled={!isFormValid()}>
|
||||
{submitLabel}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Modal>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Close Confirmation Modal */}
|
||||
{showCloseConfirmation && (
|
||||
|
||||
@@ -149,7 +149,7 @@ export default function HeadersSection({
|
||||
<Button
|
||||
onClick={handleAdd}
|
||||
variant="ghost"
|
||||
className="flex items-center justify-start gap-1 px-2 pr-4 text-sm rounded-full text-textStandard bg-bgApp border border-borderSubtle hover:border-borderStandard transition-colors min-w-[60px] h-9 [&>svg]:!size-4"
|
||||
className="flex items-center justify-start gap-1 px-2 pr-4 text-sm rounded-full text-textStandard bg-background-default border border-borderSubtle hover:border-borderStandard transition-colors min-w-[60px] h-9 [&>svg]:!size-4"
|
||||
>
|
||||
<Plus /> Add
|
||||
</Button>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Switch } from '../../../ui/switch';
|
||||
import { Gear } from '../../../icons/Gear';
|
||||
import { FixedExtensionEntry } from '../../../ConfigContext';
|
||||
import { getSubtitle, getFriendlyTitle } from './ExtensionList';
|
||||
import { Card, CardHeader, CardTitle, CardContent, CardAction } from '../../../ui/card';
|
||||
|
||||
interface ExtensionItemProps {
|
||||
extension: FixedExtensionEntry;
|
||||
@@ -70,34 +71,33 @@ export default function ExtensionItem({
|
||||
const editable = !(extension.type === 'builtin' || extension.bundled) && !isStatic;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="flex justify-between rounded-lg transition-colors border border-borderSubtle p-4 pt-3 hover:border-borderProminent hover:cursor-pointer"
|
||||
<Card
|
||||
className="transition-all duration-200 hover:shadow-default hover:cursor-pointer min-h-[120px]"
|
||||
onClick={() => handleToggle(extension)}
|
||||
>
|
||||
<div className="flex flex-col w-max-[90%] word-break">
|
||||
<h3 className="text-textStandard">{getFriendlyTitle(extension)}</h3>
|
||||
<p className="text-xs text-textSubtle">{renderSubtitle()}</p>
|
||||
</div>
|
||||
<CardHeader>
|
||||
<CardTitle className="">{getFriendlyTitle(extension)}</CardTitle>
|
||||
|
||||
<div
|
||||
className="flex items-center justify-end gap-2 w-max-[10%]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{editable && (
|
||||
<button
|
||||
className="text-textSubtle hover:text-textStandard"
|
||||
onClick={() => (onConfigure ? onConfigure(extension) : () => {})}
|
||||
>
|
||||
<Gear className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<Switch
|
||||
checked={(isToggling && visuallyEnabled) || extension.enabled}
|
||||
onCheckedChange={() => handleToggle(extension)}
|
||||
disabled={isToggling}
|
||||
variant="mono"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<CardAction onClick={(e) => e.stopPropagation()}>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{editable && (
|
||||
<button
|
||||
className="text-textSubtle hover:text-textStandard"
|
||||
onClick={() => (onConfigure ? onConfigure(extension) : () => {})}
|
||||
>
|
||||
<Gear className="h-4 w-4" />
|
||||
</button>
|
||||
)}
|
||||
<Switch
|
||||
checked={(isToggling && visuallyEnabled) || extension.enabled}
|
||||
onCheckedChange={() => handleToggle(extension)}
|
||||
disabled={isToggling}
|
||||
variant="mono"
|
||||
/>
|
||||
</div>
|
||||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent className="px-4 text-sm text-text-muted">{renderSubtitle()}</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export default function ExtensionList({
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-2 mb-2">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 2xl:grid-cols-5 gap-2">
|
||||
{sortedExtensions.map((extension) => (
|
||||
<ExtensionItem
|
||||
key={extension.name}
|
||||
|
||||
Reference in New Issue
Block a user