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:
Zane
2025-07-15 17:24:41 -07:00
committed by GitHub
parent b22f50d1a1
commit 77ea27f5f5
195 changed files with 20633 additions and 7238 deletions
@@ -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}