From 20c369b30533a2d7bbcf275487ac1372ea581cac Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 10:18:24 -0400 Subject: [PATCH 01/20] Enhanced onboarding page visual design - Added card-based layout for provider selection - Created OpenRouter and Ollama brand icon components - Implemented shimmer effect for recommended option - Added blue 'Recommended' badge for OpenRouter card - Updated layout to be wider and left-aligned - Improved visual hierarchy with consistent spacing - Added descriptive text for each provider option --- ui/desktop/src/components/ProviderGuard.tsx | 136 +++++++++++++----- ui/desktop/src/components/icons/Ollama.tsx | 45 ++++++ .../src/components/icons/OpenRouter.tsx | 44 ++++++ ui/desktop/src/components/icons/index.tsx | 4 + ui/desktop/src/styles/main.css | 15 ++ 5 files changed, 207 insertions(+), 37 deletions(-) create mode 100644 ui/desktop/src/components/icons/Ollama.tsx create mode 100644 ui/desktop/src/components/icons/OpenRouter.tsx diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 831a23b52397..745ccea24e78 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -8,6 +8,8 @@ import { initializeSystem } from '../utils/providerUtils'; import { toastService } from '../toasts'; import { OllamaSetup } from './OllamaSetup'; import { checkOllamaStatus } from '../utils/ollamaDetection'; +import { Goose } from './icons/Goose'; +import { OpenRouter, Ollama } from './icons'; interface ProviderGuardProps { children: React.ReactNode; @@ -106,8 +108,6 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { const model = (await read('GOOSE_MODEL', false)) ?? config.GOOSE_DEFAULT_MODEL; // Always check for Ollama regardless of provider status - const ollamaStatus = await checkOllamaStatus(); - setOllamaDetected(ollamaStatus.isRunning); if (provider && model) { console.log('ProviderGuard - Provider and model found, continuing normally'); @@ -116,6 +116,9 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { console.log('ProviderGuard - No provider/model configured'); setShowFirstTimeSetup(true); } + const ollamaStatus = await checkOllamaStatus(); + setOllamaDetected(ollamaStatus.isRunning); + } catch (error) { // On error, assume no provider and redirect to welcome console.error('Error checking provider configuration:', error); @@ -193,52 +196,111 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { if (showFirstTimeSetup) { return (
-
- -

Welcome to Goose!

-

- Let's get you set up with an AI provider to start using Goose. -

+
+ {/* Header section - same width as buttons, left aligned */} +
+
+
+ +
+

+ Welcome to Goose +

+
+

+ Since it's your first time here, let's get your set you with a provider so we can make incredible work together. +

+
+ {/* Setup options - same width container */}
- + {/* Primary OpenRouter Card with subtle shimmer - wrapped for badge positioning */} +
+ {/* Recommended badge - positioned relative to wrapper */} +
+ + Recommended + +
+ +
+ {/* Subtle shimmer effect */} +
+ +
+
+

+ + Automatic setup with OpenRouter +

+
+
+ + + +
+
+

+ Get instant access to multiple AI models including GPT-4, Claude, and more. Quick setup with just a few clicks. +

+
+
- - -
+

+ Run AI models locally on your computer. Completely free and private with no internet required. +

+
+ + {/* Other providers Card - outline style */} +
navigate('/welcome', { replace: true })} - className="w-full px-6 py-3 bg-background-muted text-text-standard rounded-lg hover:bg-background-hover transition-colors font-medium" + className="w-full p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group" > - Configure Other Providers (advanced) - -
+
+
+

+ Other providers +

+
+
+ + + +
+
+

+ If you've already signed up for providers like Anthropic, OpenAI etc, you can enter your own keys. +

+
-

- OpenRouter provides instant access to multiple AI models with a simple setup. - {ollamaDetected - ? ' Ollama is also detected on your system for running models locally.' - : ' You can also install Ollama to run free AI models locally on your computer.'} -

+
); diff --git a/ui/desktop/src/components/icons/Ollama.tsx b/ui/desktop/src/components/icons/Ollama.tsx new file mode 100644 index 000000000000..4e7b57d36a61 --- /dev/null +++ b/ui/desktop/src/components/icons/Ollama.tsx @@ -0,0 +1,45 @@ +export default function Ollama({ className = '' }) { + return ( + + ); +} diff --git a/ui/desktop/src/components/icons/OpenRouter.tsx b/ui/desktop/src/components/icons/OpenRouter.tsx new file mode 100644 index 000000000000..6a7d760c38e7 --- /dev/null +++ b/ui/desktop/src/components/icons/OpenRouter.tsx @@ -0,0 +1,44 @@ +export default function OpenRouter({ className = '' }) { + return ( + + ); +} diff --git a/ui/desktop/src/components/icons/index.tsx b/ui/desktop/src/components/icons/index.tsx index 7e76ab55b938..556f7dc4bc5d 100644 --- a/ui/desktop/src/components/icons/index.tsx +++ b/ui/desktop/src/components/icons/index.tsx @@ -29,6 +29,8 @@ import { Grape } from './Grape'; import Idea from './Idea'; import LinkedIn from './LinkedIn'; import More from './More'; +import OpenRouter from './OpenRouter'; +import Ollama from './Ollama'; import Refresh from './Refresh'; import SensitiveHidden from './SensitiveHidden'; import SensitiveVisible from './SensitiveVisible'; @@ -80,6 +82,8 @@ export { LinkedIn, Microphone, More, + OpenRouter, + Ollama, Refresh, SensitiveHidden, SensitiveVisible, diff --git a/ui/desktop/src/styles/main.css b/ui/desktop/src/styles/main.css index a329dabbc327..f87f088c0629 100644 --- a/ui/desktop/src/styles/main.css +++ b/ui/desktop/src/styles/main.css @@ -741,3 +741,18 @@ p > code.bg-inline-code { [data-state='collapsed'] [data-slot='sidebar-gap'] { will-change: width; } + +/* Shimmer animation for onboarding card */ +@keyframes shimmer { + 0% { + transform: translateX(-100%); + } + 100% { + transform: translateX(100%); + } +} + +.animate-shimmer { + animation: shimmer 2s infinite; +} + From 1e451e91313237d616a6c814728385a156c1a746 Mon Sep 17 00:00:00 2001 From: Spence Date: Mon, 18 Aug 2025 11:52:38 -0400 Subject: [PATCH 02/20] Pr 4156 (#4162) --- ui/desktop/src/components/ProviderGuard.tsx | 92 +++++++++++---------- 1 file changed, 50 insertions(+), 42 deletions(-) diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 745ccea24e78..c2c99936e997 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -173,7 +173,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { if (showOllamaSetup) { return ( -
+
@@ -195,29 +195,29 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { if (showFirstTimeSetup) { return ( -
+
{/* Header section - same width as buttons, left aligned */} -
-
+
+
- +
-

+

Welcome to Goose

-

+

Since it's your first time here, let's get your set you with a provider so we can make incredible work together.

{/* Setup options - same width container */} -
+
{/* Primary OpenRouter Card with subtle shimmer - wrapped for badge positioning */}
{/* Recommended badge - positioned relative to wrapper */} -
+
Recommended @@ -225,77 +225,85 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
{/* Subtle shimmer effect */}
-

- +

+ Automatic setup with OpenRouter

- +
-

+

Get instant access to multiple AI models including GPT-4, Claude, and more. Quick setup with just a few clicks.

{/* Ollama Card - outline style */} -
{ - setShowFirstTimeSetup(false); - setShowOllamaSetup(true); - }} - className="w-full p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group" - > -
-
- {ollamaDetected && ( - - )} -

- - {ollamaDetected ? 'Ollama (detected)' : 'Ollama'} -

+
+ {/* Detected badge - similar to recommended but green */} + {ollamaDetected && ( +
+ + Detected +
-
- - - + )} + +
{ + setShowFirstTimeSetup(false); + setShowOllamaSetup(true); + }} + className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group" + > +
+
+ +

+ Ollama +

+
+
+ + + +
+

+ Run AI models locally on your computer. Completely free and private with no internet required. +

-

- Run AI models locally on your computer. Completely free and private with no internet required. -

- {/* Other providers Card - outline style */} + {/* Other providers Card - outline style */}
navigate('/welcome', { replace: true })} - className="w-full p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group" + className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group" >
-

+

Other providers

- +
-

+

If you've already signed up for providers like Anthropic, OpenAI etc, you can enter your own keys.

From 8a922cbfee4924ea236f437b931f2069d86dacf9 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 12:00:30 -0400 Subject: [PATCH 03/20] Enhance shimmer effect to be more subtle and continuous - Updated shimmer animation to go back and forth smoothly - Increased duration from 2s to 6s for more subtle effect - Reduced opacity values (max 0.15, mid 0.05) for subtlety - Changed gradient from via-white/20 to via-white/8 for lighter effect - Added ease-in-out timing for smoother transitions - Removed abrupt reset by creating continuous back-and-forth motion --- ui/desktop/src/components/ProviderGuard.tsx | 2 +- ui/desktop/src/styles/main.css | 37 ++++++++++++++++++--- 2 files changed, 34 insertions(+), 5 deletions(-) diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index c2c99936e997..6ebef04e1d45 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -228,7 +228,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { className="relative w-full p-4 sm:p-6 bg-background-muted border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group overflow-hidden" > {/* Subtle shimmer effect */} -
+
diff --git a/ui/desktop/src/styles/main.css b/ui/desktop/src/styles/main.css index f87f088c0629..b43784eb6614 100644 --- a/ui/desktop/src/styles/main.css +++ b/ui/desktop/src/styles/main.css @@ -742,17 +742,46 @@ p > code.bg-inline-code { will-change: width; } -/* Shimmer animation for onboarding card */ + + 25% { + opacity: 0.3; + } + 50% { + transform: translateX(100%); + opacity: 0.1; + } + 75% { + opacity: 0.3; + } + 100% { + transform: translateX(-100%); + opacity: 0; + } +} + + +/* Subtle back-and-forth shimmer animation for onboarding card */ @keyframes shimmer { 0% { transform: translateX(-100%); + opacity: 0; } - 100% { + 20% { + opacity: 0.15; + } + 50% { transform: translateX(100%); + opacity: 0.05; + } + 80% { + opacity: 0.15; + } + 100% { + transform: translateX(-100%); + opacity: 0; } } .animate-shimmer { - animation: shimmer 2s infinite; + animation: shimmer 6s ease-in-out infinite; } - From dd10b0cce60a3c00e5fd8c6621114f19f767fbc3 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 12:11:17 -0400 Subject: [PATCH 04/20] Restore proper icon positioning above card headings - Moved OpenRouter and Ollama icons to be left-aligned above heading text - Removed inline flex layout that put icons next to text - Added mb-3 spacing between icons and headings for clean separation - Maintained responsive sizing (w-5 h-5 sm:w-6 sm:h-6) from previous work - Consistent structure across both provider cards - Icons now properly positioned as visual branding elements above content --- ui/desktop/src/components/ProviderGuard.tsx | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 6ebef04e1d45..56b79aec0138 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -232,8 +232,8 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
-

- + +

Automatic setup with OpenRouter

@@ -242,6 +242,9 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
+
+
+

Get instant access to multiple AI models including GPT-4, Claude, and more. Quick setup with just a few clicks. @@ -268,8 +271,9 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { className="w-full p-4 sm:p-6 bg-transparent border border-background-hover rounded-xl hover:border-text-muted transition-all duration-200 cursor-pointer group" >

-
- +
+
+

Ollama

From 7a8e0113021c52fc91a388e12998736ff8a6d8f1 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 12:13:51 -0400 Subject: [PATCH 05/20] Fix JSX syntax error and clean up card structure - Fixed missing closing div tag that caused ESBuild transform error - Properly structured both OpenRouter and Ollama cards with clean JSX - Icons positioned above headings with mb-3 spacing - Maintained responsive design and shimmer effects - Consistent card layout and styling across both provider options --- ui/desktop/src/components/ProviderGuard.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 56b79aec0138..1cc23dbe3834 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -242,9 +242,6 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
-
-
-

Get instant access to multiple AI models including GPT-4, Claude, and more. Quick setup with just a few clicks. @@ -272,8 +269,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { >

-
- +

Ollama

From 56fe403e274b25591972a2d7f0e8ce44b227e819 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 12:31:30 -0400 Subject: [PATCH 06/20] Fix CSS syntax error - remove orphaned keyframe rules - Removed orphaned keyframe rules (25%, 50%, 75%, 100%) that were missing their parent @keyframes declaration - Fixed 'Missing opening {' error in Tailwind CSS compilation - Balanced CSS braces (111 opening, 111 closing) - Maintained clean shimmer animation implementation --- ui/desktop/src/styles/main.css | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/ui/desktop/src/styles/main.css b/ui/desktop/src/styles/main.css index b43784eb6614..9aded362616a 100644 --- a/ui/desktop/src/styles/main.css +++ b/ui/desktop/src/styles/main.css @@ -743,21 +743,6 @@ p > code.bg-inline-code { } - 25% { - opacity: 0.3; - } - 50% { - transform: translateX(100%); - opacity: 0.1; - } - 75% { - opacity: 0.3; - } - 100% { - transform: translateX(-100%); - opacity: 0; - } -} /* Subtle back-and-forth shimmer animation for onboarding card */ From 3aab3bdac2b792ee4ec80af9789e67ad6826469f Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 12:42:42 -0400 Subject: [PATCH 07/20] Standardize provider settings header with consistent back button MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Replaced WelcomeGooseLogo with BackButton for both onboarding and settings modes - Applied consistent header pattern matching other pages (ScheduleDetailView, SessionHistoryView) - Updated layout structure: BackButton → h1 title → description → border-b - Changed heading from text-2xl to text-4xl font-light for consistency - Added proper background and text color classes (bg-background-default text-text-default) - Used border-border-default instead of custom styling - Improved responsive text sizing and spacing - Removed conditional back button logic - now always shows back button --- .../providers/ProviderSettingsPage.tsx | 39 ++++++++----------- 1 file changed, 16 insertions(+), 23 deletions(-) diff --git a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx index a0db2348bbf2..8dc11a9275f6 100644 --- a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx @@ -5,7 +5,6 @@ import ProviderGrid from './ProviderGrid'; import { useConfig } from '../../ConfigContext'; import { ProviderDetails } from '../../../api/types.gen'; import { initializeSystem } from '../../../utils/providerUtils'; -import WelcomeGooseLogo from '../../WelcomeGooseLogo'; import { toastService } from '../../../toasts'; interface ProviderSettingsProps { @@ -97,31 +96,25 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett ); return ( -
+
- {isOnboarding && ( -
-
- +
+ {/* Consistent header pattern with back button */} +
+
+
+

+ {isOnboarding ? 'Configure your providers' : 'Provider Configuration Settings'} +

+ {isOnboarding && ( +

+ Select an AI model provider to get started with goose. You'll need to use API keys + generated by each provider which will be encrypted and stored locally. You can change + your provider at any time in settings. +

+ )}
- )} -
- {/* Only show back button if not in onboarding mode */} - {!isOnboarding && } -

- {isOnboarding ? 'Configure your providers' : 'Provider Configuration Settings'} -

- {isOnboarding && ( -

- Select an AI model provider to get started with goose. You'll need to use API keys - generated by each provider which will be encrypted and stored locally. You can change - your provider at any time in settings. -

- )}
From 005a89b8f5d81eadf5c25a749585843f559fb12a Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 12:45:34 -0400 Subject: [PATCH 08/20] Fix back button spacing to prevent macOS stoplight overlap - Increased top padding from pt-6 to pt-12 to account for 32px titlebar-drag-region - Added pt-2 to back button container for better vertical spacing - Added no-drag class to back button container to ensure it remains clickable - Prevents macOS window controls (red/yellow/green buttons) from overlapping back button - Maintains consistent spacing with other pages while accounting for titlebar --- .../components/settings/providers/ProviderSettingsPage.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx index 8dc11a9275f6..ec5b965dc372 100644 --- a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx @@ -98,10 +98,10 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett return (
-
+
{/* Consistent header pattern with back button */}
-
+

From a150b18293204179045422a8c40adc5e2cce5780 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 12:47:35 -0400 Subject: [PATCH 09/20] Update onboarding header text from 'Configure your providers' to 'Other providers' - Changed onboarding mode header to be more concise and consistent - Maintains 'Provider Configuration Settings' for non-onboarding mode - Aligns with the simplified, user-friendly language used elsewhere in onboarding --- .../src/components/settings/providers/ProviderSettingsPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx index ec5b965dc372..4e30cb548f5a 100644 --- a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx @@ -105,7 +105,7 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett

- {isOnboarding ? 'Configure your providers' : 'Provider Configuration Settings'} + {isOnboarding ? 'Other providers' : 'Provider Configuration Settings'}

{isOnboarding && (

From 7f0d0784c6c55e4c57190047aaf03d206c558ae8 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 12:51:05 -0400 Subject: [PATCH 10/20] Center-align provider cards while keeping header text left-aligned - Changed ProviderGrid justifyContent from 'start' to 'center' - Provider cards now center in available space instead of left-aligning - Eliminates large gap on the right side of the grid - Header text remains left-aligned for better readability - Improves visual balance of the provider selection page --- ui/desktop/src/components/settings/providers/ProviderGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx index 0f1ce36b7b0c..661342782ae9 100644 --- a/ui/desktop/src/components/settings/providers/ProviderGrid.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderGrid.tsx @@ -14,7 +14,7 @@ const GridLayout = memo(function GridLayout({ children }: { children: React.Reac className="grid gap-4 [&_*]:z-20 p-1" style={{ gridTemplateColumns: 'repeat(auto-fill, minmax(200px, 200px))', - justifyContent: 'start', + justifyContent: 'center', }} > {children} From 153db2af0cfbb1642434c867b8f89e216b84fba3 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 12:57:28 -0400 Subject: [PATCH 11/20] Align OllamaSetup component with onboarding card design pattern - Added Ollama icon above heading with mb-3 spacing (matches onboarding cards) - Changed header from text-center to text-left alignment - Removed text-center from status messages and notifications - Imported Ollama icon component for consistent branding - Maintains left-aligned layout consistency throughout onboarding flow - Status messages now left-aligned for better readability --- ui/desktop/src/components/OllamaSetup.tsx | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/ui/desktop/src/components/OllamaSetup.tsx b/ui/desktop/src/components/OllamaSetup.tsx index 4bdff6b68986..3aae36f84211 100644 --- a/ui/desktop/src/components/OllamaSetup.tsx +++ b/ui/desktop/src/components/OllamaSetup.tsx @@ -11,6 +11,7 @@ import { } from '../utils/ollamaDetection'; import { initializeSystem } from '../utils/providerUtils'; import { toastService } from '../toasts'; +import { Ollama } from './icons'; interface OllamaSetupProps { onSuccess: () => void; @@ -144,7 +145,9 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { return (

-
+ {/* Header with icon above heading - left aligned like onboarding cards */} +
+

Ollama Setup

Ollama lets you run AI models for free, private and locally on your computer. @@ -154,7 +157,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { {ollamaDetected ? (

-

✓ Ollama is running on your system

+

✓ Ollama is running on your system

{modelStatus === 'checking' ? ( @@ -164,10 +167,10 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { ) : modelStatus === 'not-available' ? (
-

+

The {getPreferredModel()} model is not installed

-

+

This model is recommended for the best experience with Goose

@@ -182,12 +185,12 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { ) : modelStatus === 'downloading' ? (
-

+

Downloading {getPreferredModel()}...

{downloadProgress && ( <> -

+

{downloadProgress.status}

{downloadProgress.total && downloadProgress.completed && ( @@ -200,7 +203,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { }} />
-

+

{Math.round((downloadProgress.completed / downloadProgress.total) * 100)}%

@@ -222,7 +225,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { ) : (
-

Ollama is not detected on your system

+

Ollama is not detected on your system

{isPolling ? ( @@ -230,8 +233,8 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
-

Waiting for Ollama to start...

-

+

Waiting for Ollama to start...

+

Once Ollama is installed and running, we'll automatically detect it.

From 63bd7663059efb3a70d4ff1ca947d9a6696c6005 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 12:59:54 -0400 Subject: [PATCH 12/20] Increase icon-to-header spacing to 48px on welcome page - Changed OpenRouter and Ollama icon spacing from mb-3 (12px) to mb-12 (48px) - Creates more generous visual breathing room between brand icons and headings - Improves visual hierarchy and makes the cards feel less cramped - Maintains consistent spacing across both provider cards --- ui/desktop/src/components/ProviderGuard.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 1cc23dbe3834..ae61b3dedf58 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -232,7 +232,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {
- +

Automatic setup with OpenRouter

@@ -269,7 +269,7 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { >
- +

Ollama

From 795e4ece2e402652b73dd83a462fddc111d4a4e6 Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 13:08:43 -0400 Subject: [PATCH 13/20] Replace Ollama status box with detected pill badge for consistency - Replaced large success box with compact green pill badge - Changed text from 'Ollama is running on your system' to 'Ollama is detected and running' - Uses same styling as the 'Detected' badge on main onboarding page (bg-green-600, rounded-full) - Maintains visual consistency between onboarding flow and setup page - Creates cleaner, more compact status indicator --- ui/desktop/src/components/OllamaSetup.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ui/desktop/src/components/OllamaSetup.tsx b/ui/desktop/src/components/OllamaSetup.tsx index 3aae36f84211..d276bff22bc4 100644 --- a/ui/desktop/src/components/OllamaSetup.tsx +++ b/ui/desktop/src/components/OllamaSetup.tsx @@ -156,8 +156,8 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { {ollamaDetected ? (
-
-

✓ Ollama is running on your system

+
+ Ollama is detected and running
{modelStatus === 'checking' ? ( From 0c39f48c8edfa777f9ee43bd44ef36dd6883dbfd Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 13:10:11 -0400 Subject: [PATCH 14/20] Add 64px spacing between detected pill and button - Added mb-16 (64px) margin-bottom to detected pill container - Creates generous breathing room between status indicator and action buttons - Improves visual hierarchy and reduces cramped feeling - Provides clear separation between status and next steps --- ui/desktop/src/components/OllamaSetup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/desktop/src/components/OllamaSetup.tsx b/ui/desktop/src/components/OllamaSetup.tsx index d276bff22bc4..b9d94553d330 100644 --- a/ui/desktop/src/components/OllamaSetup.tsx +++ b/ui/desktop/src/components/OllamaSetup.tsx @@ -156,7 +156,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { {ollamaDetected ? (
-
+
Ollama is detected and running
From 04f62c4ecf6605b3e1c0e6884b67f4473931acdf Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 13:13:13 -0400 Subject: [PATCH 15/20] Convert 'not detected' message to pill badge on Ollama setup page - Replaced large warning box with compact orange pill badge - Changed from bg-background-warning box to bg-orange-600 rounded-full pill - Added mb-16 (64px) spacing between pill and Install button for consistency - Maintains pill badge pattern established for detected state - Only applies to secondary Ollama setup page, not main onboarding page --- ui/desktop/src/components/OllamaSetup.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ui/desktop/src/components/OllamaSetup.tsx b/ui/desktop/src/components/OllamaSetup.tsx index b9d94553d330..2507bc6dac64 100644 --- a/ui/desktop/src/components/OllamaSetup.tsx +++ b/ui/desktop/src/components/OllamaSetup.tsx @@ -166,7 +166,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
) : modelStatus === 'not-available' ? (
-
+

The {getPreferredModel()} model is not installed

@@ -224,8 +224,8 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) {
) : (
-
-

Ollama is not detected on your system

+
+ Ollama is not detected on your system
{isPolling ? ( From bbab2a4e96c36d6b632ddc356124abfbed6bd5cb Mon Sep 17 00:00:00 2001 From: spencrmartin Date: Mon, 18 Aug 2025 13:15:37 -0400 Subject: [PATCH 16/20] Simplify cancel button text from 'Use a different provider' to 'Cancel' - Changed button text to be more concise and direct - 'Cancel' is clearer and more standard for secondary actions - Reduces cognitive load and makes the action more obvious - Maintains same styling and functionality --- ui/desktop/src/components/OllamaSetup.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/desktop/src/components/OllamaSetup.tsx b/ui/desktop/src/components/OllamaSetup.tsx index 2507bc6dac64..2d104d0f9632 100644 --- a/ui/desktop/src/components/OllamaSetup.tsx +++ b/ui/desktop/src/components/OllamaSetup.tsx @@ -256,7 +256,7 @@ export function OllamaSetup({ onSuccess, onCancel }: OllamaSetupProps) { onClick={onCancel} className="w-full px-6 py-3 bg-transparent text-text-muted rounded-lg hover:bg-background-muted transition-colors" > - Use a different provider + Cancel
); From 603672d5435c3025702c21836d917ade861fc498 Mon Sep 17 00:00:00 2001 From: Spence Date: Tue, 19 Aug 2025 17:06:03 -0400 Subject: [PATCH 17/20] Fix navigation bugs after provider selection (#4204) --- ui/desktop/src/components/ProviderGuard.tsx | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index ae61b3dedf58..8a4b1b703042 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -75,6 +75,9 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { setOpenRouterSetupState(null); setShowFirstTimeSetup(false); setHasProvider(true); + + // Navigate to chat after successful setup + navigate('/', { replace: true }); } else { throw new Error('Provider or model not found after OpenRouter setup'); } @@ -182,6 +185,8 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { onSuccess={() => { setShowOllamaSetup(false); setHasProvider(true); + // Navigate to chat after successful setup + navigate('/', { replace: true }); }} onCancel={() => { setShowOllamaSetup(false); From ab9eb8766b124c313fc1e8a62400a7da1b530be5 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Tue, 19 Aug 2025 15:13:43 -0700 Subject: [PATCH 18/20] fix tests --- ui/desktop/src/api/client.gen.ts | 2 +- ui/desktop/src/api/client/client.gen.ts | 199 -------- ui/desktop/src/api/client/index.ts | 25 - ui/desktop/src/api/client/types.gen.ts | 232 --------- ui/desktop/src/api/client/utils.gen.ts | 440 ------------------ ui/desktop/src/api/core/auth.gen.ts | 42 -- ui/desktop/src/api/core/bodySerializer.gen.ts | 92 ---- ui/desktop/src/api/core/params.gen.ts | 153 ------ ui/desktop/src/api/core/pathSerializer.gen.ts | 181 ------- ui/desktop/src/api/core/types.gen.ts | 120 ----- ui/desktop/src/api/sdk.gen.ts | 118 ++--- ui/desktop/src/api/types.gen.ts | 4 +- .../src/components/OllamaSetup.test.tsx | 6 +- .../providers/ProviderSettingsPage.tsx | 6 +- ui/desktop/tests/e2e/app.spec.ts | 140 +++--- 15 files changed, 137 insertions(+), 1623 deletions(-) delete mode 100644 ui/desktop/src/api/client/client.gen.ts delete mode 100644 ui/desktop/src/api/client/index.ts delete mode 100644 ui/desktop/src/api/client/types.gen.ts delete mode 100644 ui/desktop/src/api/client/utils.gen.ts delete mode 100644 ui/desktop/src/api/core/auth.gen.ts delete mode 100644 ui/desktop/src/api/core/bodySerializer.gen.ts delete mode 100644 ui/desktop/src/api/core/params.gen.ts delete mode 100644 ui/desktop/src/api/core/pathSerializer.gen.ts delete mode 100644 ui/desktop/src/api/core/types.gen.ts diff --git a/ui/desktop/src/api/client.gen.ts b/ui/desktop/src/api/client.gen.ts index 163da4e54e92..6759c1f28e3b 100644 --- a/ui/desktop/src/api/client.gen.ts +++ b/ui/desktop/src/api/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { ClientOptions } from './types.gen'; -import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from './client'; +import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch'; /** * The `createClientConfig()` function will be called on client initialization diff --git a/ui/desktop/src/api/client/client.gen.ts b/ui/desktop/src/api/client/client.gen.ts deleted file mode 100644 index 0c606b81c603..000000000000 --- a/ui/desktop/src/api/client/client.gen.ts +++ /dev/null @@ -1,199 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Client, Config, ResolvedRequestOptions } from './types.gen'; -import { - buildUrl, - createConfig, - createInterceptors, - getParseAs, - mergeConfigs, - mergeHeaders, - setAuthParams, -} from './utils.gen'; - -type ReqInit = Omit & { - body?: any; - headers: ReturnType; -}; - -export const createClient = (config: Config = {}): Client => { - let _config = mergeConfigs(createConfig(), config); - - const getConfig = (): Config => ({ ..._config }); - - const setConfig = (config: Config): Config => { - _config = mergeConfigs(_config, config); - return getConfig(); - }; - - const interceptors = createInterceptors< - Request, - Response, - unknown, - ResolvedRequestOptions - >(); - - const request: Client['request'] = async (options) => { - const opts = { - ..._config, - ...options, - fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, - headers: mergeHeaders(_config.headers, options.headers), - serializedBody: undefined, - }; - - if (opts.security) { - await setAuthParams({ - ...opts, - security: opts.security, - }); - } - - if (opts.requestValidator) { - await opts.requestValidator(opts); - } - - if (opts.body && opts.bodySerializer) { - opts.serializedBody = opts.bodySerializer(opts.body); - } - - // remove Content-Type header if body is empty to avoid sending invalid requests - if (opts.serializedBody === undefined || opts.serializedBody === '') { - opts.headers.delete('Content-Type'); - } - - const url = buildUrl(opts); - const requestInit: ReqInit = { - redirect: 'follow', - ...opts, - body: opts.serializedBody, - }; - - let request = new Request(url, requestInit); - - for (const fn of interceptors.request._fns) { - if (fn) { - request = await fn(request, opts); - } - } - - // fetch must be assigned here, otherwise it would throw the error: - // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation - const _fetch = opts.fetch!; - let response = await _fetch(request); - - for (const fn of interceptors.response._fns) { - if (fn) { - response = await fn(response, request, opts); - } - } - - const result = { - request, - response, - }; - - if (response.ok) { - if ( - response.status === 204 || - response.headers.get('Content-Length') === '0' - ) { - return opts.responseStyle === 'data' - ? {} - : { - data: {}, - ...result, - }; - } - - const parseAs = - (opts.parseAs === 'auto' - ? getParseAs(response.headers.get('Content-Type')) - : opts.parseAs) ?? 'json'; - - let data: any; - switch (parseAs) { - case 'arrayBuffer': - case 'blob': - case 'formData': - case 'json': - case 'text': - data = await response[parseAs](); - break; - case 'stream': - return opts.responseStyle === 'data' - ? response.body - : { - data: response.body, - ...result, - }; - } - - if (parseAs === 'json') { - if (opts.responseValidator) { - await opts.responseValidator(data); - } - - if (opts.responseTransformer) { - data = await opts.responseTransformer(data); - } - } - - return opts.responseStyle === 'data' - ? data - : { - data, - ...result, - }; - } - - const textError = await response.text(); - let jsonError: unknown; - - try { - jsonError = JSON.parse(textError); - } catch { - // noop - } - - const error = jsonError ?? textError; - let finalError = error; - - for (const fn of interceptors.error._fns) { - if (fn) { - finalError = (await fn(error, response, request, opts)) as string; - } - } - - finalError = finalError || ({} as string); - - if (opts.throwOnError) { - throw finalError; - } - - // TODO: we probably want to return error and improve types - return opts.responseStyle === 'data' - ? undefined - : { - error: finalError, - ...result, - }; - }; - - return { - buildUrl, - connect: (options) => request({ ...options, method: 'CONNECT' }), - delete: (options) => request({ ...options, method: 'DELETE' }), - get: (options) => request({ ...options, method: 'GET' }), - getConfig, - head: (options) => request({ ...options, method: 'HEAD' }), - interceptors, - options: (options) => request({ ...options, method: 'OPTIONS' }), - patch: (options) => request({ ...options, method: 'PATCH' }), - post: (options) => request({ ...options, method: 'POST' }), - put: (options) => request({ ...options, method: 'PUT' }), - request, - setConfig, - trace: (options) => request({ ...options, method: 'TRACE' }), - }; -}; diff --git a/ui/desktop/src/api/client/index.ts b/ui/desktop/src/api/client/index.ts deleted file mode 100644 index 318a84b6a800..000000000000 --- a/ui/desktop/src/api/client/index.ts +++ /dev/null @@ -1,25 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type { Auth } from '../core/auth.gen'; -export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; -export { - formDataBodySerializer, - jsonBodySerializer, - urlSearchParamsBodySerializer, -} from '../core/bodySerializer.gen'; -export { buildClientParams } from '../core/params.gen'; -export { createClient } from './client.gen'; -export type { - Client, - ClientOptions, - Config, - CreateClientConfig, - Options, - OptionsLegacyParser, - RequestOptions, - RequestResult, - ResolvedRequestOptions, - ResponseStyle, - TDataShape, -} from './types.gen'; -export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/ui/desktop/src/api/client/types.gen.ts b/ui/desktop/src/api/client/types.gen.ts deleted file mode 100644 index 2a123be9a16b..000000000000 --- a/ui/desktop/src/api/client/types.gen.ts +++ /dev/null @@ -1,232 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth } from '../core/auth.gen'; -import type { - Client as CoreClient, - Config as CoreConfig, -} from '../core/types.gen'; -import type { Middleware } from './utils.gen'; - -export type ResponseStyle = 'data' | 'fields'; - -export interface Config - extends Omit, - CoreConfig { - /** - * Base URL for all requests made by this client. - */ - baseUrl?: T['baseUrl']; - /** - * Fetch API implementation. You can use this option to provide a custom - * fetch instance. - * - * @default globalThis.fetch - */ - fetch?: (request: Request) => ReturnType; - /** - * Please don't use the Fetch client for Next.js applications. The `next` - * options won't have any effect. - * - * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. - */ - next?: never; - /** - * Return the response data parsed in a specified format. By default, `auto` - * will infer the appropriate method from the `Content-Type` response header. - * You can override this behavior with any of the {@link Body} methods. - * Select `stream` if you don't want to parse response data at all. - * - * @default 'auto' - */ - parseAs?: - | 'arrayBuffer' - | 'auto' - | 'blob' - | 'formData' - | 'json' - | 'stream' - | 'text'; - /** - * Should we return only data or multiple fields (data, error, response, etc.)? - * - * @default 'fields' - */ - responseStyle?: ResponseStyle; - /** - * Throw an error instead of returning it in the response? - * - * @default false - */ - throwOnError?: T['throwOnError']; -} - -export interface RequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends Config<{ - responseStyle: TResponseStyle; - throwOnError: ThrowOnError; - }> { - /** - * Any body that you want to add to your request. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} - */ - body?: unknown; - path?: Record; - query?: Record; - /** - * Security mechanism(s) to use for the request. - */ - security?: ReadonlyArray; - url: Url; -} - -export interface ResolvedRequestOptions< - TResponseStyle extends ResponseStyle = 'fields', - ThrowOnError extends boolean = boolean, - Url extends string = string, -> extends RequestOptions { - serializedBody?: string; -} - -export type RequestResult< - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = ThrowOnError extends true - ? Promise< - TResponseStyle extends 'data' - ? TData extends Record - ? TData[keyof TData] - : TData - : { - data: TData extends Record - ? TData[keyof TData] - : TData; - request: Request; - response: Response; - } - > - : Promise< - TResponseStyle extends 'data' - ? - | (TData extends Record - ? TData[keyof TData] - : TData) - | undefined - : ( - | { - data: TData extends Record - ? TData[keyof TData] - : TData; - error: undefined; - } - | { - data: undefined; - error: TError extends Record - ? TError[keyof TError] - : TError; - } - ) & { - request: Request; - response: Response; - } - >; - -export interface ClientOptions { - baseUrl?: string; - responseStyle?: ResponseStyle; - throwOnError?: boolean; -} - -type MethodFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'>, -) => RequestResult; - -type RequestFn = < - TData = unknown, - TError = unknown, - ThrowOnError extends boolean = false, - TResponseStyle extends ResponseStyle = 'fields', ->( - options: Omit, 'method'> & - Pick>, 'method'>, -) => RequestResult; - -type BuildUrlFn = < - TData extends { - body?: unknown; - path?: Record; - query?: Record; - url: string; - }, ->( - options: Pick & Options, -) => string; - -export type Client = CoreClient & { - interceptors: Middleware; -}; - -/** - * The `createClientConfig()` function will be called on client initialization - * and the returned object will become the client's initial configuration. - * - * You may want to initialize your client this way instead of calling - * `setConfig()`. This is useful for example if you're using Next.js - * to ensure your client always has the correct values. - */ -export type CreateClientConfig = ( - override?: Config, -) => Config & T>; - -export interface TDataShape { - body?: unknown; - headers?: unknown; - path?: unknown; - query?: unknown; - url: string; -} - -type OmitKeys = Pick>; - -export type Options< - TData extends TDataShape = TDataShape, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = OmitKeys< - RequestOptions, - 'body' | 'path' | 'query' | 'url' -> & - Omit; - -export type OptionsLegacyParser< - TData = unknown, - ThrowOnError extends boolean = boolean, - TResponseStyle extends ResponseStyle = 'fields', -> = TData extends { body?: any } - ? TData extends { headers?: any } - ? OmitKeys< - RequestOptions, - 'body' | 'headers' | 'url' - > & - TData - : OmitKeys, 'body' | 'url'> & - TData & - Pick, 'headers'> - : TData extends { headers?: any } - ? OmitKeys< - RequestOptions, - 'headers' | 'url' - > & - TData & - Pick, 'body'> - : OmitKeys, 'url'> & TData; diff --git a/ui/desktop/src/api/client/utils.gen.ts b/ui/desktop/src/api/client/utils.gen.ts deleted file mode 100644 index 1ee09c6db7e4..000000000000 --- a/ui/desktop/src/api/client/utils.gen.ts +++ /dev/null @@ -1,440 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import { getAuthToken } from '../core/auth.gen'; -import type { - QuerySerializer, - QuerySerializerOptions, -} from '../core/bodySerializer.gen'; -import { jsonBodySerializer } from '../core/bodySerializer.gen'; -import { - serializeArrayParam, - serializeObjectParam, - serializePrimitiveParam, -} from '../core/pathSerializer.gen'; -import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; - -interface PathSerializer { - path: Record; - url: string; -} - -const PATH_PARAM_RE = /\{[^{}]+\}/g; - -type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -type ArraySeparatorStyle = ArrayStyle | MatrixStyle; - -const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { - let url = _url; - const matches = _url.match(PATH_PARAM_RE); - if (matches) { - for (const match of matches) { - let explode = false; - let name = match.substring(1, match.length - 1); - let style: ArraySeparatorStyle = 'simple'; - - if (name.endsWith('*')) { - explode = true; - name = name.substring(0, name.length - 1); - } - - if (name.startsWith('.')) { - name = name.substring(1); - style = 'label'; - } else if (name.startsWith(';')) { - name = name.substring(1); - style = 'matrix'; - } - - const value = path[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - url = url.replace( - match, - serializeArrayParam({ explode, name, style, value }), - ); - continue; - } - - if (typeof value === 'object') { - url = url.replace( - match, - serializeObjectParam({ - explode, - name, - style, - value: value as Record, - valueOnly: true, - }), - ); - continue; - } - - if (style === 'matrix') { - url = url.replace( - match, - `;${serializePrimitiveParam({ - name, - value: value as string, - })}`, - ); - continue; - } - - const replaceValue = encodeURIComponent( - style === 'label' ? `.${value as string}` : (value as string), - ); - url = url.replace(match, replaceValue); - } - } - return url; -}; - -export const createQuerySerializer = ({ - allowReserved, - array, - object, -}: QuerySerializerOptions = {}) => { - const querySerializer = (queryParams: T) => { - const search: string[] = []; - if (queryParams && typeof queryParams === 'object') { - for (const name in queryParams) { - const value = queryParams[name]; - - if (value === undefined || value === null) { - continue; - } - - if (Array.isArray(value)) { - const serializedArray = serializeArrayParam({ - allowReserved, - explode: true, - name, - style: 'form', - value, - ...array, - }); - if (serializedArray) search.push(serializedArray); - } else if (typeof value === 'object') { - const serializedObject = serializeObjectParam({ - allowReserved, - explode: true, - name, - style: 'deepObject', - value: value as Record, - ...object, - }); - if (serializedObject) search.push(serializedObject); - } else { - const serializedPrimitive = serializePrimitiveParam({ - allowReserved, - name, - value: value as string, - }); - if (serializedPrimitive) search.push(serializedPrimitive); - } - } - } - return search.join('&'); - }; - return querySerializer; -}; - -/** - * Infers parseAs value from provided Content-Type header. - */ -export const getParseAs = ( - contentType: string | null, -): Exclude => { - if (!contentType) { - // If no Content-Type header is provided, the best we can do is return the raw response body, - // which is effectively the same as the 'stream' option. - return 'stream'; - } - - const cleanContent = contentType.split(';')[0]?.trim(); - - if (!cleanContent) { - return; - } - - if ( - cleanContent.startsWith('application/json') || - cleanContent.endsWith('+json') - ) { - return 'json'; - } - - if (cleanContent === 'multipart/form-data') { - return 'formData'; - } - - if ( - ['application/', 'audio/', 'image/', 'video/'].some((type) => - cleanContent.startsWith(type), - ) - ) { - return 'blob'; - } - - if (cleanContent.startsWith('text/')) { - return 'text'; - } - - return; -}; - -const checkForExistence = ( - options: Pick & { - headers: Headers; - }, - name?: string, -): boolean => { - if (!name) { - return false; - } - if ( - options.headers.has(name) || - options.query?.[name] || - options.headers.get('Cookie')?.includes(`${name}=`) - ) { - return true; - } - return false; -}; - -export const setAuthParams = async ({ - security, - ...options -}: Pick, 'security'> & - Pick & { - headers: Headers; - }) => { - for (const auth of security) { - if (checkForExistence(options, auth.name)) { - continue; - } - - const token = await getAuthToken(auth, options.auth); - - if (!token) { - continue; - } - - const name = auth.name ?? 'Authorization'; - - switch (auth.in) { - case 'query': - if (!options.query) { - options.query = {}; - } - options.query[name] = token; - break; - case 'cookie': - options.headers.append('Cookie', `${name}=${token}`); - break; - case 'header': - default: - options.headers.set(name, token); - break; - } - } -}; - -export const buildUrl: Client['buildUrl'] = (options) => { - const url = getUrl({ - baseUrl: options.baseUrl as string, - path: options.path, - query: options.query, - querySerializer: - typeof options.querySerializer === 'function' - ? options.querySerializer - : createQuerySerializer(options.querySerializer), - url: options.url, - }); - return url; -}; - -export const getUrl = ({ - baseUrl, - path, - query, - querySerializer, - url: _url, -}: { - baseUrl?: string; - path?: Record; - query?: Record; - querySerializer: QuerySerializer; - url: string; -}) => { - const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; - let url = (baseUrl ?? '') + pathUrl; - if (path) { - url = defaultPathSerializer({ path, url }); - } - let search = query ? querySerializer(query) : ''; - if (search.startsWith('?')) { - search = search.substring(1); - } - if (search) { - url += `?${search}`; - } - return url; -}; - -export const mergeConfigs = (a: Config, b: Config): Config => { - const config = { ...a, ...b }; - if (config.baseUrl?.endsWith('/')) { - config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); - } - config.headers = mergeHeaders(a.headers, b.headers); - return config; -}; - -export const mergeHeaders = ( - ...headers: Array['headers'] | undefined> -): Headers => { - const mergedHeaders = new Headers(); - for (const header of headers) { - if (!header || typeof header !== 'object') { - continue; - } - - const iterator = - header instanceof Headers ? header.entries() : Object.entries(header); - - for (const [key, value] of iterator) { - if (value === null) { - mergedHeaders.delete(key); - } else if (Array.isArray(value)) { - for (const v of value) { - mergedHeaders.append(key, v as string); - } - } else if (value !== undefined) { - // assume object headers are meant to be JSON stringified, i.e. their - // content value in OpenAPI specification is 'application/json' - mergedHeaders.set( - key, - typeof value === 'object' ? JSON.stringify(value) : (value as string), - ); - } - } - } - return mergedHeaders; -}; - -type ErrInterceptor = ( - error: Err, - response: Res, - request: Req, - options: Options, -) => Err | Promise; - -type ReqInterceptor = ( - request: Req, - options: Options, -) => Req | Promise; - -type ResInterceptor = ( - response: Res, - request: Req, - options: Options, -) => Res | Promise; - -class Interceptors { - _fns: (Interceptor | null)[]; - - constructor() { - this._fns = []; - } - - clear() { - this._fns = []; - } - - getInterceptorIndex(id: number | Interceptor): number { - if (typeof id === 'number') { - return this._fns[id] ? id : -1; - } else { - return this._fns.indexOf(id); - } - } - exists(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - return !!this._fns[index]; - } - - eject(id: number | Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = null; - } - } - - update(id: number | Interceptor, fn: Interceptor) { - const index = this.getInterceptorIndex(id); - if (this._fns[index]) { - this._fns[index] = fn; - return id; - } else { - return false; - } - } - - use(fn: Interceptor) { - this._fns = [...this._fns, fn]; - return this._fns.length - 1; - } -} - -// `createInterceptors()` response, meant for external use as it does not -// expose internals -export interface Middleware { - error: Pick< - Interceptors>, - 'eject' | 'use' - >; - request: Pick>, 'eject' | 'use'>; - response: Pick< - Interceptors>, - 'eject' | 'use' - >; -} - -// do not add `Middleware` as return type so we can use _fns internally -export const createInterceptors = () => ({ - error: new Interceptors>(), - request: new Interceptors>(), - response: new Interceptors>(), -}); - -const defaultQuerySerializer = createQuerySerializer({ - allowReserved: false, - array: { - explode: true, - style: 'form', - }, - object: { - explode: true, - style: 'deepObject', - }, -}); - -const defaultHeaders = { - 'Content-Type': 'application/json', -}; - -export const createConfig = ( - override: Config & T> = {}, -): Config & T> => ({ - ...jsonBodySerializer, - headers: defaultHeaders, - parseAs: 'auto', - querySerializer: defaultQuerySerializer, - ...override, -}); diff --git a/ui/desktop/src/api/core/auth.gen.ts b/ui/desktop/src/api/core/auth.gen.ts deleted file mode 100644 index f8a73266f934..000000000000 --- a/ui/desktop/src/api/core/auth.gen.ts +++ /dev/null @@ -1,42 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -export type AuthToken = string | undefined; - -export interface Auth { - /** - * Which part of the request do we use to send the auth? - * - * @default 'header' - */ - in?: 'header' | 'query' | 'cookie'; - /** - * Header or query parameter name. - * - * @default 'Authorization' - */ - name?: string; - scheme?: 'basic' | 'bearer'; - type: 'apiKey' | 'http'; -} - -export const getAuthToken = async ( - auth: Auth, - callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, -): Promise => { - const token = - typeof callback === 'function' ? await callback(auth) : callback; - - if (!token) { - return; - } - - if (auth.scheme === 'bearer') { - return `Bearer ${token}`; - } - - if (auth.scheme === 'basic') { - return `Basic ${btoa(token)}`; - } - - return token; -}; diff --git a/ui/desktop/src/api/core/bodySerializer.gen.ts b/ui/desktop/src/api/core/bodySerializer.gen.ts deleted file mode 100644 index 49cd8925e3bd..000000000000 --- a/ui/desktop/src/api/core/bodySerializer.gen.ts +++ /dev/null @@ -1,92 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { - ArrayStyle, - ObjectStyle, - SerializerOptions, -} from './pathSerializer.gen'; - -export type QuerySerializer = (query: Record) => string; - -export type BodySerializer = (body: any) => any; - -export interface QuerySerializerOptions { - allowReserved?: boolean; - array?: SerializerOptions; - object?: SerializerOptions; -} - -const serializeFormDataPair = ( - data: FormData, - key: string, - value: unknown, -): void => { - if (typeof value === 'string' || value instanceof Blob) { - data.append(key, value); - } else if (value instanceof Date) { - data.append(key, value.toISOString()); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -const serializeUrlSearchParamsPair = ( - data: URLSearchParams, - key: string, - value: unknown, -): void => { - if (typeof value === 'string') { - data.append(key, value); - } else { - data.append(key, JSON.stringify(value)); - } -}; - -export const formDataBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): FormData => { - const data = new FormData(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeFormDataPair(data, key, v)); - } else { - serializeFormDataPair(data, key, value); - } - }); - - return data; - }, -}; - -export const jsonBodySerializer = { - bodySerializer: (body: T): string => - JSON.stringify(body, (_key, value) => - typeof value === 'bigint' ? value.toString() : value, - ), -}; - -export const urlSearchParamsBodySerializer = { - bodySerializer: | Array>>( - body: T, - ): string => { - const data = new URLSearchParams(); - - Object.entries(body).forEach(([key, value]) => { - if (value === undefined || value === null) { - return; - } - if (Array.isArray(value)) { - value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); - } else { - serializeUrlSearchParamsPair(data, key, value); - } - }); - - return data.toString(); - }, -}; diff --git a/ui/desktop/src/api/core/params.gen.ts b/ui/desktop/src/api/core/params.gen.ts deleted file mode 100644 index 71c88e852b72..000000000000 --- a/ui/desktop/src/api/core/params.gen.ts +++ /dev/null @@ -1,153 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -type Slot = 'body' | 'headers' | 'path' | 'query'; - -export type Field = - | { - in: Exclude; - /** - * Field name. This is the name we want the user to see and use. - */ - key: string; - /** - * Field mapped name. This is the name we want to use in the request. - * If omitted, we use the same value as `key`. - */ - map?: string; - } - | { - in: Extract; - /** - * Key isn't required for bodies. - */ - key?: string; - map?: string; - }; - -export interface Fields { - allowExtra?: Partial>; - args?: ReadonlyArray; -} - -export type FieldsConfig = ReadonlyArray; - -const extraPrefixesMap: Record = { - $body_: 'body', - $headers_: 'headers', - $path_: 'path', - $query_: 'query', -}; -const extraPrefixes = Object.entries(extraPrefixesMap); - -type KeyMap = Map< - string, - { - in: Slot; - map?: string; - } ->; - -const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { - if (!map) { - map = new Map(); - } - - for (const config of fields) { - if ('in' in config) { - if (config.key) { - map.set(config.key, { - in: config.in, - map: config.map, - }); - } - } else if (config.args) { - buildKeyMap(config.args, map); - } - } - - return map; -}; - -interface Params { - body: unknown; - headers: Record; - path: Record; - query: Record; -} - -const stripEmptySlots = (params: Params) => { - for (const [slot, value] of Object.entries(params)) { - if (value && typeof value === 'object' && !Object.keys(value).length) { - delete params[slot as Slot]; - } - } -}; - -export const buildClientParams = ( - args: ReadonlyArray, - fields: FieldsConfig, -) => { - const params: Params = { - body: {}, - headers: {}, - path: {}, - query: {}, - }; - - const map = buildKeyMap(fields); - - let config: FieldsConfig[number] | undefined; - - for (const [index, arg] of args.entries()) { - if (fields[index]) { - config = fields[index]; - } - - if (!config) { - continue; - } - - if ('in' in config) { - if (config.key) { - const field = map.get(config.key)!; - const name = field.map || config.key; - (params[field.in] as Record)[name] = arg; - } else { - params.body = arg; - } - } else { - for (const [key, value] of Object.entries(arg ?? {})) { - const field = map.get(key); - - if (field) { - const name = field.map || key; - (params[field.in] as Record)[name] = value; - } else { - const extra = extraPrefixes.find(([prefix]) => - key.startsWith(prefix), - ); - - if (extra) { - const [prefix, slot] = extra; - (params[slot] as Record)[ - key.slice(prefix.length) - ] = value; - } else { - for (const [slot, allowed] of Object.entries( - config.allowExtra ?? {}, - )) { - if (allowed) { - (params[slot as Slot] as Record)[key] = value; - break; - } - } - } - } - } - } - } - - stripEmptySlots(params); - - return params; -}; diff --git a/ui/desktop/src/api/core/pathSerializer.gen.ts b/ui/desktop/src/api/core/pathSerializer.gen.ts deleted file mode 100644 index 8d9993104743..000000000000 --- a/ui/desktop/src/api/core/pathSerializer.gen.ts +++ /dev/null @@ -1,181 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -interface SerializeOptions - extends SerializePrimitiveOptions, - SerializerOptions {} - -interface SerializePrimitiveOptions { - allowReserved?: boolean; - name: string; -} - -export interface SerializerOptions { - /** - * @default true - */ - explode: boolean; - style: T; -} - -export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; -export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; -type MatrixStyle = 'label' | 'matrix' | 'simple'; -export type ObjectStyle = 'form' | 'deepObject'; -type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; - -interface SerializePrimitiveParam extends SerializePrimitiveOptions { - value: string; -} - -export const separatorArrayExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { - switch (style) { - case 'form': - return ','; - case 'pipeDelimited': - return '|'; - case 'spaceDelimited': - return '%20'; - default: - return ','; - } -}; - -export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { - switch (style) { - case 'label': - return '.'; - case 'matrix': - return ';'; - case 'simple': - return ','; - default: - return '&'; - } -}; - -export const serializeArrayParam = ({ - allowReserved, - explode, - name, - style, - value, -}: SerializeOptions & { - value: unknown[]; -}) => { - if (!explode) { - const joinedValues = ( - allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) - ).join(separatorArrayNoExplode(style)); - switch (style) { - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - case 'simple': - return joinedValues; - default: - return `${name}=${joinedValues}`; - } - } - - const separator = separatorArrayExplode(style); - const joinedValues = value - .map((v) => { - if (style === 'label' || style === 'simple') { - return allowReserved ? v : encodeURIComponent(v as string); - } - - return serializePrimitiveParam({ - allowReserved, - name, - value: v as string, - }); - }) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; - -export const serializePrimitiveParam = ({ - allowReserved, - name, - value, -}: SerializePrimitiveParam) => { - if (value === undefined || value === null) { - return ''; - } - - if (typeof value === 'object') { - throw new Error( - 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', - ); - } - - return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; -}; - -export const serializeObjectParam = ({ - allowReserved, - explode, - name, - style, - value, - valueOnly, -}: SerializeOptions & { - value: Record | Date; - valueOnly?: boolean; -}) => { - if (value instanceof Date) { - return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; - } - - if (style !== 'deepObject' && !explode) { - let values: string[] = []; - Object.entries(value).forEach(([key, v]) => { - values = [ - ...values, - key, - allowReserved ? (v as string) : encodeURIComponent(v as string), - ]; - }); - const joinedValues = values.join(','); - switch (style) { - case 'form': - return `${name}=${joinedValues}`; - case 'label': - return `.${joinedValues}`; - case 'matrix': - return `;${name}=${joinedValues}`; - default: - return joinedValues; - } - } - - const separator = separatorObjectExplode(style); - const joinedValues = Object.entries(value) - .map(([key, v]) => - serializePrimitiveParam({ - allowReserved, - name: style === 'deepObject' ? `${name}[${key}]` : key, - value: v as string, - }), - ) - .join(separator); - return style === 'label' || style === 'matrix' - ? separator + joinedValues - : joinedValues; -}; diff --git a/ui/desktop/src/api/core/types.gen.ts b/ui/desktop/src/api/core/types.gen.ts deleted file mode 100644 index 5bfae35c0af1..000000000000 --- a/ui/desktop/src/api/core/types.gen.ts +++ /dev/null @@ -1,120 +0,0 @@ -// This file is auto-generated by @hey-api/openapi-ts - -import type { Auth, AuthToken } from './auth.gen'; -import type { - BodySerializer, - QuerySerializer, - QuerySerializerOptions, -} from './bodySerializer.gen'; - -export interface Client< - RequestFn = never, - Config = unknown, - MethodFn = never, - BuildUrlFn = never, -> { - /** - * Returns the final request URL. - */ - buildUrl: BuildUrlFn; - connect: MethodFn; - delete: MethodFn; - get: MethodFn; - getConfig: () => Config; - head: MethodFn; - options: MethodFn; - patch: MethodFn; - post: MethodFn; - put: MethodFn; - request: RequestFn; - setConfig: (config: Config) => Config; - trace: MethodFn; -} - -export interface Config { - /** - * Auth token or a function returning auth token. The resolved value will be - * added to the request payload as defined by its `security` array. - */ - auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; - /** - * A function for serializing request body parameter. By default, - * {@link JSON.stringify()} will be used. - */ - bodySerializer?: BodySerializer | null; - /** - * An object containing any HTTP headers that you want to pre-populate your - * `Headers` object with. - * - * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} - */ - headers?: - | RequestInit['headers'] - | Record< - string, - | string - | number - | boolean - | (string | number | boolean)[] - | null - | undefined - | unknown - >; - /** - * The request method. - * - * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} - */ - method?: - | 'CONNECT' - | 'DELETE' - | 'GET' - | 'HEAD' - | 'OPTIONS' - | 'PATCH' - | 'POST' - | 'PUT' - | 'TRACE'; - /** - * A function for serializing request query parameters. By default, arrays - * will be exploded in form style, objects will be exploded in deepObject - * style, and reserved characters are percent-encoded. - * - * This method will have no effect if the native `paramsSerializer()` Axios - * API function is used. - * - * {@link https://swagger.io/docs/specification/serialization/#query View examples} - */ - querySerializer?: QuerySerializer | QuerySerializerOptions; - /** - * A function validating request data. This is useful if you want to ensure - * the request conforms to the desired shape, so it can be safely sent to - * the server. - */ - requestValidator?: (data: unknown) => Promise; - /** - * A function transforming response data before it's returned. This is useful - * for post-processing data, e.g. converting ISO strings into Date objects. - */ - responseTransformer?: (data: unknown) => Promise; - /** - * A function validating response data. This is useful if you want to ensure - * the response conforms to the desired shape, so it can be safely passed to - * the transformers and returned to the user. - */ - responseValidator?: (data: unknown) => Promise; -} - -type IsExactlyNeverOrNeverUndefined = [T] extends [never] - ? true - : [T] extends [never | undefined] - ? [undefined] extends [T] - ? false - : true - : false; - -export type OmitNever> = { - [K in keyof T as IsExactlyNeverOrNeverUndefined extends true - ? never - : K]: T[K]; -}; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index e5407d98db3d..703749ff11e5 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Options as ClientOptions, TDataShape, Client } from './client'; -import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors } from './types.gen'; +import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; +import type { AddSubRecipesData, AddSubRecipesResponse2, ExtendPromptData, ExtendPromptResponse2, UpdateSessionConfigData, UpdateSessionConfigResponse, GetToolsData, GetToolsResponse, UpdateAgentProviderData, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponse, ReadAllConfigData, ReadAllConfigResponse, BackupConfigData, BackupConfigResponse, CreateCustomProviderData, CreateCustomProviderResponse, RemoveCustomProviderData, RemoveCustomProviderResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, InitConfigData, InitConfigResponse, UpsertPermissionsData, UpsertPermissionsResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RecoverConfigData, RecoverConfigResponse, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse, ValidateConfigData, ValidateConfigResponse, ConfirmPermissionData, ManageContextData, ManageContextResponse, CreateRecipeData, CreateRecipeResponse2, DecodeRecipeData, DecodeRecipeResponse2, EncodeRecipeData, EncodeRecipeResponse2, ScanRecipeData, ScanRecipeResponse2, CreateScheduleData, CreateScheduleResponse, DeleteScheduleData, DeleteScheduleResponse, ListSchedulesData, ListSchedulesResponse2, UpdateScheduleData, UpdateScheduleResponse, InspectRunningJobData, InspectRunningJobResponse, KillRunningJobData, PauseScheduleData, PauseScheduleResponse, RunNowHandlerData, RunNowHandlerResponse, SessionsHandlerData, SessionsHandlerResponse, UnpauseScheduleData, UnpauseScheduleResponse, ListSessionsData, ListSessionsResponse, GetSessionHistoryData, GetSessionHistoryResponse } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -19,210 +19,210 @@ export type Options(options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/agent/add_sub_recipes', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const extendPrompt = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/agent/prompt', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const updateSessionConfig = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/agent/session_config', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const getTools = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/agent/tools', ...options }); }; export const updateAgentProvider = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/agent/update_provider', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const updateRouterToolSelector = (options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? _heyApiClient).post({ url: '/agent/update_router_tool_selector', ...options }); }; export const readAllConfig = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/config', ...options }); }; export const backupConfig = (options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? _heyApiClient).post({ url: '/config/backup', ...options }); }; export const createCustomProvider = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/custom-providers', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const removeCustomProvider = (options: Options) => { - return (options.client ?? _heyApiClient).delete({ + return (options.client ?? _heyApiClient).delete({ url: '/config/custom-providers/{id}', ...options }); }; export const getExtensions = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/config/extensions', ...options }); }; export const addExtension = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/extensions', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const removeExtension = (options: Options) => { - return (options.client ?? _heyApiClient).delete({ + return (options.client ?? _heyApiClient).delete({ url: '/config/extensions/{name}', ...options }); }; export const initConfig = (options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? _heyApiClient).post({ url: '/config/init', ...options }); }; export const upsertPermissions = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/permissions', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const providers = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/config/providers', ...options }); }; export const readConfig = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/read', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const recoverConfig = (options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? _heyApiClient).post({ url: '/config/recover', ...options }); }; export const removeConfig = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/remove', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const upsertConfig = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/upsert', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const validateConfig = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/config/validate', ...options }); }; export const confirmPermission = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/confirm', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const manageContext = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/context/manage', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; @@ -231,136 +231,136 @@ export const manageContext = (options: Opt * Create a Recipe configuration from the current session */ export const createRecipe = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/recipes/create', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const decodeRecipe = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/recipes/decode', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const encodeRecipe = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/recipes/encode', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const scanRecipe = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/recipes/scan', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const createSchedule = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/schedule/create', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const deleteSchedule = (options: Options) => { - return (options.client ?? _heyApiClient).delete({ + return (options.client ?? _heyApiClient).delete({ url: '/schedule/delete/{id}', ...options }); }; export const listSchedules = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/schedule/list', ...options }); }; export const updateSchedule = (options: Options) => { - return (options.client ?? _heyApiClient).put({ + return (options.client ?? _heyApiClient).put({ url: '/schedule/{id}', ...options, headers: { 'Content-Type': 'application/json', - ...options.headers + ...options?.headers } }); }; export const inspectRunningJob = (options: Options) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? _heyApiClient).get({ url: '/schedule/{id}/inspect', ...options }); }; export const killRunningJob = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/schedule/{id}/kill', ...options }); }; export const pauseSchedule = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/schedule/{id}/pause', ...options }); }; export const runNowHandler = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/schedule/{id}/run_now', ...options }); }; export const sessionsHandler = (options: Options) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? _heyApiClient).get({ url: '/schedule/{id}/sessions', ...options }); }; export const unpauseSchedule = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/schedule/{id}/unpause', ...options }); }; export const listSessions = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/sessions', ...options }); }; export const getSessionHistory = (options: Options) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? _heyApiClient).get({ url: '/sessions/{session_id}', ...options }); diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index a400fffd14d0..abae88556db8 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -59,9 +59,7 @@ export type ConfigKeyQuery = { }; export type ConfigResponse = { - config: { - [key: string]: unknown; - }; + config: {}; }; export type Content = RawTextContent | RawImageContent | RawEmbeddedResource | Annotated; diff --git a/ui/desktop/src/components/OllamaSetup.test.tsx b/ui/desktop/src/components/OllamaSetup.test.tsx index 291610e4a3b1..945a8beb0831 100644 --- a/ui/desktop/src/components/OllamaSetup.test.tsx +++ b/ui/desktop/src/components/OllamaSetup.test.tsx @@ -81,7 +81,7 @@ describe('OllamaSetup', () => { render(); await waitFor(() => { - fireEvent.click(screen.getByText('Use a different provider')); + fireEvent.click(screen.getByText('Cancel')); }); expect(mockOnCancel).toHaveBeenCalled(); @@ -156,7 +156,7 @@ describe('OllamaSetup', () => { render(); await waitFor(() => { - expect(screen.getByText(/Ollama is running on your system/)).toBeInTheDocument(); + expect(screen.getByText(/Ollama is detected and running/)).toBeInTheDocument(); }); }); @@ -252,7 +252,7 @@ describe('OllamaSetup', () => { pollCallback!({ isRunning: true, host: 'http://127.0.0.1:11434' }); await waitFor(() => { - expect(screen.getByText('✓ Ollama is running on your system')).toBeInTheDocument(); + expect(screen.getByText('Ollama is detected and running')).toBeInTheDocument(); }); }); }); diff --git a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx index 4e30cb548f5a..37aa7c373aca 100644 --- a/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx +++ b/ui/desktop/src/components/settings/providers/ProviderSettingsPage.tsx @@ -104,14 +104,14 @@ export default function ProviderSettings({ onClose, isOnboarding }: ProviderSett
-

+

{isOnboarding ? 'Other providers' : 'Provider Configuration Settings'}

{isOnboarding && (

Select an AI model provider to get started with goose. You'll need to use API keys - generated by each provider which will be encrypted and stored locally. You can change - your provider at any time in settings. + generated by each provider which will be encrypted and stored locally. You can + change your provider at any time in settings.

)}
diff --git a/ui/desktop/tests/e2e/app.spec.ts b/ui/desktop/tests/e2e/app.spec.ts index 227b703e9075..3b5ce2b91cb6 100644 --- a/ui/desktop/tests/e2e/app.spec.ts +++ b/ui/desktop/tests/e2e/app.spec.ts @@ -37,11 +37,11 @@ test.beforeEach(async ({ }, testInfo) => { if (mainWindow) { // Get a clean test name without the full hierarchy const testName = testInfo.titlePath[testInfo.titlePath.length - 1]; - + // Get provider name if we're in a provider suite const providerSuite = testInfo.titlePath.find(t => t.startsWith('Provider:')); const providerName = providerSuite ? providerSuite.split(': ')[1] : undefined; - + console.log(`Setting overlay for test: "${testName}"${providerName ? ` (Provider: ${providerName})` : ''}`); await showTestName(mainWindow, testName, providerName); } @@ -56,9 +56,9 @@ test.afterEach(async () => { // Helper function to select a provider async function selectProvider(mainWindow: any, provider: Provider) { console.log(`Selecting provider: ${provider.name}`); - + // If we're already in the chat interface, we need to reset providers - const chatTextarea = await mainWindow.waitForSelector('[data-testid="chat-input"]', { + const chatTextarea = await mainWindow.waitForSelector('[data-testid="chat-input"]', { timeout: 2000 }).catch(() => null); @@ -76,7 +76,7 @@ async function selectProvider(mainWindow: any, provider: Provider) { timeout: 5000, state: 'visible' }); - + const modelsTab = await mainWindow.waitForSelector('[data-testid="settings-models-tab"]'); await modelsTab.click(); @@ -90,7 +90,7 @@ async function selectProvider(mainWindow: any, provider: Provider) { state: 'visible' }); await resetButton.click(); - + // Wait for the reset to complete await mainWindow.waitForTimeout(1000); } @@ -117,13 +117,13 @@ async function selectProvider(mainWindow: any, provider: Provider) { } // Check if we need to click "configure other providers (advanced)" button - const configureAdvancedButton = await mainWindow.waitForSelector('button:has-text("configure other providers (advanced)")', { + const configureAdvancedButton = await mainWindow.waitForSelector('h3:has-text("Other providers")', { timeout: 3000, state: 'visible' }).catch(() => null); if (configureAdvancedButton) { - console.log('Found "configure other providers (advanced)" button, clicking it...'); + console.log('Found "configure other providers" button, clicking it...'); await configureAdvancedButton.click(); await mainWindow.waitForTimeout(1500); } @@ -213,7 +213,7 @@ test.describe('Goose App', () => { // Get the main window once for all tests mainWindow = await electronApp.firstWindow(); await mainWindow.waitForLoadState('domcontentloaded'); - + // Try to wait for networkidle, but don't fail if it times out due to MCP activity try { await mainWindow.waitForLoadState('networkidle', { timeout: 10000 }); @@ -307,13 +307,13 @@ test.describe('Goose App', () => { timeout: 5000, state: 'visible' }); - + const appTab = await mainWindow.waitForSelector('[data-testid="settings-app-tab"]'); await appTab.click(); // Wait for the theme selector to be visible await mainWindow.waitForTimeout(1000); - + // Find and click the dark mode toggle button const darkModeButton = await mainWindow.waitForSelector('[data-testid="dark-mode-button"]'); const lightModeButton = await mainWindow.waitForSelector('[data-testid="light-mode-button"]'); @@ -341,13 +341,13 @@ test.describe('Goose App', () => { // check that system mode is clickable await systemModeButton.click(); - + // Toggle back to light mode await lightModeButton.click(); - + // Pause to show return to original state await mainWindow.waitForTimeout(2000); - + // Navigate back to home const homeButton = await mainWindow.waitForSelector('[data-testid="sidebar-home-button"]'); await homeButton.click(); @@ -364,75 +364,75 @@ test.describe('Goose App', () => { test.describe('Chat', () => { test('chat interaction', async () => { console.log(`Testing chat interaction with ${provider.name}...`); - + // Find the chat input const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]'); expect(await chatInput.isVisible()).toBe(true); - + // Type a message await chatInput.fill('Hello, can you help me with a simple task?'); - + // Take screenshot before sending await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-before-send.png` }); - + // Send message await chatInput.press('Enter'); - + // Wait for loading indicator to appear console.log('Waiting for loading indicator...'); const loadingGoose = await mainWindow.waitForSelector('[data-testid="loading-indicator"]', { timeout: 2000 }); expect(await loadingGoose.isVisible()).toBe(true); - + // Take screenshot of loading state await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-loading-state.png` }); - + // Wait for loading indicator to disappear console.log('Waiting for response...'); await mainWindow.waitForSelector('[data-testid="loading-indicator"]', { state: 'hidden', timeout: 30000 }); - + // Get the latest response const response = await mainWindow.locator('[data-testid="message-container"]').last(); expect(await response.isVisible()).toBe(true); - + // Verify response has content const responseText = await response.textContent(); expect(responseText).toBeTruthy(); expect(responseText.length).toBeGreaterThan(0); - + // Take screenshot of response await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-chat-response.png` }); }); - + test('verify chat history', async () => { console.log(`Testing chat history with ${provider.name}...`); - + // Find the chat input again const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]'); - + // Test message sending with a specific question await chatInput.fill('What is 2+2?'); - + // Send message await chatInput.press('Enter'); - + // Wait for loading indicator and response await mainWindow.waitForSelector('[data-testid="loading-indicator"]', { state: 'hidden', timeout: 30000 }); - + // Get the latest response const response = await mainWindow.locator('[data-testid="message-container"]').last(); const responseText = await response.textContent(); expect(responseText).toBeTruthy(); - + // Check for message history const messages = await mainWindow.locator('[data-testid="message-container"]').all(); expect(messages.length).toBeGreaterThanOrEqual(2); - + // Take screenshot of chat history await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-chat-history.png` }); - + // Test command history (up arrow) await chatInput.press('Control+ArrowUp'); const inputValue = await chatInput.inputValue(); @@ -443,7 +443,7 @@ test.describe('Goose App', () => { test.describe('MCP Integration', () => { test('running quotes MCP server integration', async () => { console.log(`Testing Running Quotes MCP server integration with ${provider.name}...`); - + // Create test-results directory if it doesn't exist const fs = require('fs'); if (!fs.existsSync('test-results')) { @@ -460,7 +460,7 @@ test.describe('Goose App', () => { console.log('NetworkIdle timeout (likely due to MCP activity), continuing with test...'); } await mainWindow.waitForLoadState('domcontentloaded'); - + // Wait for React app to be ready await mainWindow.waitForFunction(() => { const root = document.getElementById('root'); @@ -477,47 +477,47 @@ test.describe('Goose App', () => { state: 'visible' }); await extensionsButton.click(); - + // Wait for extensions page to load await mainWindow.waitForTimeout(1000); - + // Look for Running Quotes extension card console.log('Looking for existing Running Quotes extension...'); const existingExtension = await mainWindow.$('div.flex:has-text("Running Quotes")'); - + if (existingExtension) { console.log('Found existing Running Quotes extension, removing it...'); - + // Find and click the settings gear icon next to Running Quotes const settingsButton = await existingExtension.$('button[aria-label="Extension settings"]'); if (settingsButton) { await settingsButton.click(); - + // Wait for modal to appear await mainWindow.waitForTimeout(500); - + // Click the Remove Extension button const removeButton = await mainWindow.waitForSelector('button:has-text("Remove Extension")', { timeout: 2000, state: 'visible' }); await removeButton.click(); - + // Wait for confirmation modal await mainWindow.waitForTimeout(500); - + // Click the Remove button in confirmation dialog const confirmButton = await mainWindow.waitForSelector('button:has-text("Remove")', { timeout: 2000, state: 'visible' }); await confirmButton.click(); - + // Wait for extension to be removed await mainWindow.waitForTimeout(1000); } } - + // Now proceed with adding the extension console.log('Proceeding with adding Running Quotes extension...'); @@ -527,11 +527,11 @@ test.describe('Goose App', () => { timeout: 2000, state: 'visible' }); - + // Verify add extension button is visible const isAddExtensionVisible = await addExtensionButton.isVisible(); console.log('Add custom extension button visible:', isAddExtensionVisible); - + await addExtensionButton.click(); console.log('Clicked Add custom extension'); @@ -541,21 +541,21 @@ test.describe('Goose App', () => { // Fill the form console.log('Filling form fields...'); - + // Fill Extension Name const nameInput = await mainWindow.waitForSelector('input[placeholder="Enter extension name..."]', { timeout: 2000, state: 'visible' }); await nameInput.fill('Running Quotes'); - + // Fill Description const descriptionInput = await mainWindow.waitForSelector('input[placeholder="Optional description..."]', { timeout: 2000, state: 'visible' }); await descriptionInput.fill('Inspirational running quotes MCP server'); - + // Fill Command const mcpScriptPath = join(__dirname, 'basic-mcp.ts'); const commandInput = await mainWindow.waitForSelector('input[placeholder="e.g. npx -y @modelcontextprotocol/my-extension "]', { @@ -576,50 +576,50 @@ test.describe('Goose App', () => { timeout: 2000, state: 'visible' }); - + // Verify button is visible const isModalAddButtonVisible = await modalAddButton.isVisible(); console.log('Add Extension button visible:', isModalAddButtonVisible); // Click the button await modalAddButton.click(); - + console.log('Clicked Add Extension button'); // Wait for the Running Quotes extension to appear in the list console.log('Waiting for Running Quotes extension to appear...'); try { const extensionCard = await mainWindow.waitForSelector( - 'div.flex:has-text("Running Quotes")', + 'div.flex:has-text("Running Quotes")', { timeout: 30000, state: 'visible' } ); - + // Verify the extension is enabled await mainWindow.waitForTimeout(1000); const toggleButton = await extensionCard.$('button[role="switch"][data-state="checked"]'); const isEnabled = !!toggleButton; console.log('Extension enabled:', isEnabled); - + if (!isEnabled) { throw new Error('Running Quotes extension was added but not enabled'); } - + await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-extension-added.png` }); console.log('Extension added successfully'); } catch (error) { console.error('Error verifying extension:', error); - + // Get any error messages that might be visible - const errorElements = await mainWindow.$$eval('.text-red-500, .text-error', + const errorElements = await mainWindow.$$eval('.text-red-500, .text-error', elements => elements.map(el => el.textContent) ); if (errorElements.length > 0) { console.log('Found error messages:', errorElements); } - + throw error; } @@ -631,36 +631,36 @@ test.describe('Goose App', () => { } catch (error) { // Take error screenshot and log details await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-error.png` }); - + // Get page content const pageContent = await mainWindow.evaluate(() => document.body.innerHTML); console.log('Page content at error:', pageContent); - + console.error('Test failed:', error); throw error; } }); - + test('test running quotes functionality', async () => { console.log(`Testing running quotes functionality with ${provider.name}...`); - + // Find the chat input const chatInput = await mainWindow.waitForSelector('[data-testid="chat-input"]'); expect(await chatInput.isVisible()).toBe(true); - + // Type a message requesting a running quote await chatInput.fill('Can you give me an inspirational running quote using the runningQuotes tool?'); - + // Take screenshot before sending await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-before-quote-request.png` }); - + // Send message await chatInput.press('Enter'); // Get the latest response const response = await mainWindow.waitForSelector('.goose-message-tool', { timeout: 5000 }); expect(await response.isVisible()).toBe(true); - + // Click the Output dropdown to reveal the actual quote await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-response-debug.png` }); @@ -668,12 +668,12 @@ test.describe('Goose App', () => { const outputContent = await mainWindow.waitForSelector('.whitespace-pre-wrap', { timeout: 5000 }); const outputText = await outputContent.textContent(); console.log('Output text:', outputText); - + // Take screenshot of expanded response await mainWindow.screenshot({ path: `test-results/${provider.name.toLowerCase()}-quote-response.png` }); - + // Check if the output contains one of our known quotes - const containsKnownQuote = runningQuotes.some(({ quote, author }) => + const containsKnownQuote = runningQuotes.some(({ quote, author }) => outputText.includes(`"${quote}" - ${author}`) ); expect(containsKnownQuote).toBe(true); From ae24f9e0667d21d2105fe346c924b153cd406515 Mon Sep 17 00:00:00 2001 From: Zane Staggs Date: Tue, 19 Aug 2025 15:22:50 -0700 Subject: [PATCH 19/20] revert accidental changes --- ui/desktop/src/api/client.gen.ts | 2 +- ui/desktop/src/api/client/client.gen.ts | 199 ++++++++ ui/desktop/src/api/client/index.ts | 25 + ui/desktop/src/api/client/types.gen.ts | 232 +++++++++ ui/desktop/src/api/client/utils.gen.ts | 440 ++++++++++++++++++ ui/desktop/src/api/core/auth.gen.ts | 42 ++ ui/desktop/src/api/core/bodySerializer.gen.ts | 92 ++++ ui/desktop/src/api/core/params.gen.ts | 153 ++++++ ui/desktop/src/api/core/pathSerializer.gen.ts | 181 +++++++ ui/desktop/src/api/core/types.gen.ts | 120 +++++ ui/desktop/src/api/sdk.gen.ts | 118 ++--- ui/desktop/src/api/types.gen.ts | 4 +- 12 files changed, 1547 insertions(+), 61 deletions(-) create mode 100644 ui/desktop/src/api/client/client.gen.ts create mode 100644 ui/desktop/src/api/client/index.ts create mode 100644 ui/desktop/src/api/client/types.gen.ts create mode 100644 ui/desktop/src/api/client/utils.gen.ts create mode 100644 ui/desktop/src/api/core/auth.gen.ts create mode 100644 ui/desktop/src/api/core/bodySerializer.gen.ts create mode 100644 ui/desktop/src/api/core/params.gen.ts create mode 100644 ui/desktop/src/api/core/pathSerializer.gen.ts create mode 100644 ui/desktop/src/api/core/types.gen.ts diff --git a/ui/desktop/src/api/client.gen.ts b/ui/desktop/src/api/client.gen.ts index 6759c1f28e3b..163da4e54e92 100644 --- a/ui/desktop/src/api/client.gen.ts +++ b/ui/desktop/src/api/client.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts import type { ClientOptions } from './types.gen'; -import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from '@hey-api/client-fetch'; +import { type Config, type ClientOptions as DefaultClientOptions, createClient, createConfig } from './client'; /** * The `createClientConfig()` function will be called on client initialization diff --git a/ui/desktop/src/api/client/client.gen.ts b/ui/desktop/src/api/client/client.gen.ts new file mode 100644 index 000000000000..0c606b81c603 --- /dev/null +++ b/ui/desktop/src/api/client/client.gen.ts @@ -0,0 +1,199 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Client, Config, ResolvedRequestOptions } from './types.gen'; +import { + buildUrl, + createConfig, + createInterceptors, + getParseAs, + mergeConfigs, + mergeHeaders, + setAuthParams, +} from './utils.gen'; + +type ReqInit = Omit & { + body?: any; + headers: ReturnType; +}; + +export const createClient = (config: Config = {}): Client => { + let _config = mergeConfigs(createConfig(), config); + + const getConfig = (): Config => ({ ..._config }); + + const setConfig = (config: Config): Config => { + _config = mergeConfigs(_config, config); + return getConfig(); + }; + + const interceptors = createInterceptors< + Request, + Response, + unknown, + ResolvedRequestOptions + >(); + + const request: Client['request'] = async (options) => { + const opts = { + ..._config, + ...options, + fetch: options.fetch ?? _config.fetch ?? globalThis.fetch, + headers: mergeHeaders(_config.headers, options.headers), + serializedBody: undefined, + }; + + if (opts.security) { + await setAuthParams({ + ...opts, + security: opts.security, + }); + } + + if (opts.requestValidator) { + await opts.requestValidator(opts); + } + + if (opts.body && opts.bodySerializer) { + opts.serializedBody = opts.bodySerializer(opts.body); + } + + // remove Content-Type header if body is empty to avoid sending invalid requests + if (opts.serializedBody === undefined || opts.serializedBody === '') { + opts.headers.delete('Content-Type'); + } + + const url = buildUrl(opts); + const requestInit: ReqInit = { + redirect: 'follow', + ...opts, + body: opts.serializedBody, + }; + + let request = new Request(url, requestInit); + + for (const fn of interceptors.request._fns) { + if (fn) { + request = await fn(request, opts); + } + } + + // fetch must be assigned here, otherwise it would throw the error: + // TypeError: Failed to execute 'fetch' on 'Window': Illegal invocation + const _fetch = opts.fetch!; + let response = await _fetch(request); + + for (const fn of interceptors.response._fns) { + if (fn) { + response = await fn(response, request, opts); + } + } + + const result = { + request, + response, + }; + + if (response.ok) { + if ( + response.status === 204 || + response.headers.get('Content-Length') === '0' + ) { + return opts.responseStyle === 'data' + ? {} + : { + data: {}, + ...result, + }; + } + + const parseAs = + (opts.parseAs === 'auto' + ? getParseAs(response.headers.get('Content-Type')) + : opts.parseAs) ?? 'json'; + + let data: any; + switch (parseAs) { + case 'arrayBuffer': + case 'blob': + case 'formData': + case 'json': + case 'text': + data = await response[parseAs](); + break; + case 'stream': + return opts.responseStyle === 'data' + ? response.body + : { + data: response.body, + ...result, + }; + } + + if (parseAs === 'json') { + if (opts.responseValidator) { + await opts.responseValidator(data); + } + + if (opts.responseTransformer) { + data = await opts.responseTransformer(data); + } + } + + return opts.responseStyle === 'data' + ? data + : { + data, + ...result, + }; + } + + const textError = await response.text(); + let jsonError: unknown; + + try { + jsonError = JSON.parse(textError); + } catch { + // noop + } + + const error = jsonError ?? textError; + let finalError = error; + + for (const fn of interceptors.error._fns) { + if (fn) { + finalError = (await fn(error, response, request, opts)) as string; + } + } + + finalError = finalError || ({} as string); + + if (opts.throwOnError) { + throw finalError; + } + + // TODO: we probably want to return error and improve types + return opts.responseStyle === 'data' + ? undefined + : { + error: finalError, + ...result, + }; + }; + + return { + buildUrl, + connect: (options) => request({ ...options, method: 'CONNECT' }), + delete: (options) => request({ ...options, method: 'DELETE' }), + get: (options) => request({ ...options, method: 'GET' }), + getConfig, + head: (options) => request({ ...options, method: 'HEAD' }), + interceptors, + options: (options) => request({ ...options, method: 'OPTIONS' }), + patch: (options) => request({ ...options, method: 'PATCH' }), + post: (options) => request({ ...options, method: 'POST' }), + put: (options) => request({ ...options, method: 'PUT' }), + request, + setConfig, + trace: (options) => request({ ...options, method: 'TRACE' }), + }; +}; diff --git a/ui/desktop/src/api/client/index.ts b/ui/desktop/src/api/client/index.ts new file mode 100644 index 000000000000..318a84b6a800 --- /dev/null +++ b/ui/desktop/src/api/client/index.ts @@ -0,0 +1,25 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type { Auth } from '../core/auth.gen'; +export type { QuerySerializerOptions } from '../core/bodySerializer.gen'; +export { + formDataBodySerializer, + jsonBodySerializer, + urlSearchParamsBodySerializer, +} from '../core/bodySerializer.gen'; +export { buildClientParams } from '../core/params.gen'; +export { createClient } from './client.gen'; +export type { + Client, + ClientOptions, + Config, + CreateClientConfig, + Options, + OptionsLegacyParser, + RequestOptions, + RequestResult, + ResolvedRequestOptions, + ResponseStyle, + TDataShape, +} from './types.gen'; +export { createConfig, mergeHeaders } from './utils.gen'; diff --git a/ui/desktop/src/api/client/types.gen.ts b/ui/desktop/src/api/client/types.gen.ts new file mode 100644 index 000000000000..2a123be9a16b --- /dev/null +++ b/ui/desktop/src/api/client/types.gen.ts @@ -0,0 +1,232 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth } from '../core/auth.gen'; +import type { + Client as CoreClient, + Config as CoreConfig, +} from '../core/types.gen'; +import type { Middleware } from './utils.gen'; + +export type ResponseStyle = 'data' | 'fields'; + +export interface Config + extends Omit, + CoreConfig { + /** + * Base URL for all requests made by this client. + */ + baseUrl?: T['baseUrl']; + /** + * Fetch API implementation. You can use this option to provide a custom + * fetch instance. + * + * @default globalThis.fetch + */ + fetch?: (request: Request) => ReturnType; + /** + * Please don't use the Fetch client for Next.js applications. The `next` + * options won't have any effect. + * + * Install {@link https://www.npmjs.com/package/@hey-api/client-next `@hey-api/client-next`} instead. + */ + next?: never; + /** + * Return the response data parsed in a specified format. By default, `auto` + * will infer the appropriate method from the `Content-Type` response header. + * You can override this behavior with any of the {@link Body} methods. + * Select `stream` if you don't want to parse response data at all. + * + * @default 'auto' + */ + parseAs?: + | 'arrayBuffer' + | 'auto' + | 'blob' + | 'formData' + | 'json' + | 'stream' + | 'text'; + /** + * Should we return only data or multiple fields (data, error, response, etc.)? + * + * @default 'fields' + */ + responseStyle?: ResponseStyle; + /** + * Throw an error instead of returning it in the response? + * + * @default false + */ + throwOnError?: T['throwOnError']; +} + +export interface RequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends Config<{ + responseStyle: TResponseStyle; + throwOnError: ThrowOnError; + }> { + /** + * Any body that you want to add to your request. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#body} + */ + body?: unknown; + path?: Record; + query?: Record; + /** + * Security mechanism(s) to use for the request. + */ + security?: ReadonlyArray; + url: Url; +} + +export interface ResolvedRequestOptions< + TResponseStyle extends ResponseStyle = 'fields', + ThrowOnError extends boolean = boolean, + Url extends string = string, +> extends RequestOptions { + serializedBody?: string; +} + +export type RequestResult< + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = ThrowOnError extends true + ? Promise< + TResponseStyle extends 'data' + ? TData extends Record + ? TData[keyof TData] + : TData + : { + data: TData extends Record + ? TData[keyof TData] + : TData; + request: Request; + response: Response; + } + > + : Promise< + TResponseStyle extends 'data' + ? + | (TData extends Record + ? TData[keyof TData] + : TData) + | undefined + : ( + | { + data: TData extends Record + ? TData[keyof TData] + : TData; + error: undefined; + } + | { + data: undefined; + error: TError extends Record + ? TError[keyof TError] + : TError; + } + ) & { + request: Request; + response: Response; + } + >; + +export interface ClientOptions { + baseUrl?: string; + responseStyle?: ResponseStyle; + throwOnError?: boolean; +} + +type MethodFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'>, +) => RequestResult; + +type RequestFn = < + TData = unknown, + TError = unknown, + ThrowOnError extends boolean = false, + TResponseStyle extends ResponseStyle = 'fields', +>( + options: Omit, 'method'> & + Pick>, 'method'>, +) => RequestResult; + +type BuildUrlFn = < + TData extends { + body?: unknown; + path?: Record; + query?: Record; + url: string; + }, +>( + options: Pick & Options, +) => string; + +export type Client = CoreClient & { + interceptors: Middleware; +}; + +/** + * The `createClientConfig()` function will be called on client initialization + * and the returned object will become the client's initial configuration. + * + * You may want to initialize your client this way instead of calling + * `setConfig()`. This is useful for example if you're using Next.js + * to ensure your client always has the correct values. + */ +export type CreateClientConfig = ( + override?: Config, +) => Config & T>; + +export interface TDataShape { + body?: unknown; + headers?: unknown; + path?: unknown; + query?: unknown; + url: string; +} + +type OmitKeys = Pick>; + +export type Options< + TData extends TDataShape = TDataShape, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = OmitKeys< + RequestOptions, + 'body' | 'path' | 'query' | 'url' +> & + Omit; + +export type OptionsLegacyParser< + TData = unknown, + ThrowOnError extends boolean = boolean, + TResponseStyle extends ResponseStyle = 'fields', +> = TData extends { body?: any } + ? TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'body' | 'headers' | 'url' + > & + TData + : OmitKeys, 'body' | 'url'> & + TData & + Pick, 'headers'> + : TData extends { headers?: any } + ? OmitKeys< + RequestOptions, + 'headers' | 'url' + > & + TData & + Pick, 'body'> + : OmitKeys, 'url'> & TData; diff --git a/ui/desktop/src/api/client/utils.gen.ts b/ui/desktop/src/api/client/utils.gen.ts new file mode 100644 index 000000000000..1ee09c6db7e4 --- /dev/null +++ b/ui/desktop/src/api/client/utils.gen.ts @@ -0,0 +1,440 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import { getAuthToken } from '../core/auth.gen'; +import type { + QuerySerializer, + QuerySerializerOptions, +} from '../core/bodySerializer.gen'; +import { jsonBodySerializer } from '../core/bodySerializer.gen'; +import { + serializeArrayParam, + serializeObjectParam, + serializePrimitiveParam, +} from '../core/pathSerializer.gen'; +import type { Client, ClientOptions, Config, RequestOptions } from './types.gen'; + +interface PathSerializer { + path: Record; + url: string; +} + +const PATH_PARAM_RE = /\{[^{}]+\}/g; + +type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +type ArraySeparatorStyle = ArrayStyle | MatrixStyle; + +const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => { + let url = _url; + const matches = _url.match(PATH_PARAM_RE); + if (matches) { + for (const match of matches) { + let explode = false; + let name = match.substring(1, match.length - 1); + let style: ArraySeparatorStyle = 'simple'; + + if (name.endsWith('*')) { + explode = true; + name = name.substring(0, name.length - 1); + } + + if (name.startsWith('.')) { + name = name.substring(1); + style = 'label'; + } else if (name.startsWith(';')) { + name = name.substring(1); + style = 'matrix'; + } + + const value = path[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + url = url.replace( + match, + serializeArrayParam({ explode, name, style, value }), + ); + continue; + } + + if (typeof value === 'object') { + url = url.replace( + match, + serializeObjectParam({ + explode, + name, + style, + value: value as Record, + valueOnly: true, + }), + ); + continue; + } + + if (style === 'matrix') { + url = url.replace( + match, + `;${serializePrimitiveParam({ + name, + value: value as string, + })}`, + ); + continue; + } + + const replaceValue = encodeURIComponent( + style === 'label' ? `.${value as string}` : (value as string), + ); + url = url.replace(match, replaceValue); + } + } + return url; +}; + +export const createQuerySerializer = ({ + allowReserved, + array, + object, +}: QuerySerializerOptions = {}) => { + const querySerializer = (queryParams: T) => { + const search: string[] = []; + if (queryParams && typeof queryParams === 'object') { + for (const name in queryParams) { + const value = queryParams[name]; + + if (value === undefined || value === null) { + continue; + } + + if (Array.isArray(value)) { + const serializedArray = serializeArrayParam({ + allowReserved, + explode: true, + name, + style: 'form', + value, + ...array, + }); + if (serializedArray) search.push(serializedArray); + } else if (typeof value === 'object') { + const serializedObject = serializeObjectParam({ + allowReserved, + explode: true, + name, + style: 'deepObject', + value: value as Record, + ...object, + }); + if (serializedObject) search.push(serializedObject); + } else { + const serializedPrimitive = serializePrimitiveParam({ + allowReserved, + name, + value: value as string, + }); + if (serializedPrimitive) search.push(serializedPrimitive); + } + } + } + return search.join('&'); + }; + return querySerializer; +}; + +/** + * Infers parseAs value from provided Content-Type header. + */ +export const getParseAs = ( + contentType: string | null, +): Exclude => { + if (!contentType) { + // If no Content-Type header is provided, the best we can do is return the raw response body, + // which is effectively the same as the 'stream' option. + return 'stream'; + } + + const cleanContent = contentType.split(';')[0]?.trim(); + + if (!cleanContent) { + return; + } + + if ( + cleanContent.startsWith('application/json') || + cleanContent.endsWith('+json') + ) { + return 'json'; + } + + if (cleanContent === 'multipart/form-data') { + return 'formData'; + } + + if ( + ['application/', 'audio/', 'image/', 'video/'].some((type) => + cleanContent.startsWith(type), + ) + ) { + return 'blob'; + } + + if (cleanContent.startsWith('text/')) { + return 'text'; + } + + return; +}; + +const checkForExistence = ( + options: Pick & { + headers: Headers; + }, + name?: string, +): boolean => { + if (!name) { + return false; + } + if ( + options.headers.has(name) || + options.query?.[name] || + options.headers.get('Cookie')?.includes(`${name}=`) + ) { + return true; + } + return false; +}; + +export const setAuthParams = async ({ + security, + ...options +}: Pick, 'security'> & + Pick & { + headers: Headers; + }) => { + for (const auth of security) { + if (checkForExistence(options, auth.name)) { + continue; + } + + const token = await getAuthToken(auth, options.auth); + + if (!token) { + continue; + } + + const name = auth.name ?? 'Authorization'; + + switch (auth.in) { + case 'query': + if (!options.query) { + options.query = {}; + } + options.query[name] = token; + break; + case 'cookie': + options.headers.append('Cookie', `${name}=${token}`); + break; + case 'header': + default: + options.headers.set(name, token); + break; + } + } +}; + +export const buildUrl: Client['buildUrl'] = (options) => { + const url = getUrl({ + baseUrl: options.baseUrl as string, + path: options.path, + query: options.query, + querySerializer: + typeof options.querySerializer === 'function' + ? options.querySerializer + : createQuerySerializer(options.querySerializer), + url: options.url, + }); + return url; +}; + +export const getUrl = ({ + baseUrl, + path, + query, + querySerializer, + url: _url, +}: { + baseUrl?: string; + path?: Record; + query?: Record; + querySerializer: QuerySerializer; + url: string; +}) => { + const pathUrl = _url.startsWith('/') ? _url : `/${_url}`; + let url = (baseUrl ?? '') + pathUrl; + if (path) { + url = defaultPathSerializer({ path, url }); + } + let search = query ? querySerializer(query) : ''; + if (search.startsWith('?')) { + search = search.substring(1); + } + if (search) { + url += `?${search}`; + } + return url; +}; + +export const mergeConfigs = (a: Config, b: Config): Config => { + const config = { ...a, ...b }; + if (config.baseUrl?.endsWith('/')) { + config.baseUrl = config.baseUrl.substring(0, config.baseUrl.length - 1); + } + config.headers = mergeHeaders(a.headers, b.headers); + return config; +}; + +export const mergeHeaders = ( + ...headers: Array['headers'] | undefined> +): Headers => { + const mergedHeaders = new Headers(); + for (const header of headers) { + if (!header || typeof header !== 'object') { + continue; + } + + const iterator = + header instanceof Headers ? header.entries() : Object.entries(header); + + for (const [key, value] of iterator) { + if (value === null) { + mergedHeaders.delete(key); + } else if (Array.isArray(value)) { + for (const v of value) { + mergedHeaders.append(key, v as string); + } + } else if (value !== undefined) { + // assume object headers are meant to be JSON stringified, i.e. their + // content value in OpenAPI specification is 'application/json' + mergedHeaders.set( + key, + typeof value === 'object' ? JSON.stringify(value) : (value as string), + ); + } + } + } + return mergedHeaders; +}; + +type ErrInterceptor = ( + error: Err, + response: Res, + request: Req, + options: Options, +) => Err | Promise; + +type ReqInterceptor = ( + request: Req, + options: Options, +) => Req | Promise; + +type ResInterceptor = ( + response: Res, + request: Req, + options: Options, +) => Res | Promise; + +class Interceptors { + _fns: (Interceptor | null)[]; + + constructor() { + this._fns = []; + } + + clear() { + this._fns = []; + } + + getInterceptorIndex(id: number | Interceptor): number { + if (typeof id === 'number') { + return this._fns[id] ? id : -1; + } else { + return this._fns.indexOf(id); + } + } + exists(id: number | Interceptor) { + const index = this.getInterceptorIndex(id); + return !!this._fns[index]; + } + + eject(id: number | Interceptor) { + const index = this.getInterceptorIndex(id); + if (this._fns[index]) { + this._fns[index] = null; + } + } + + update(id: number | Interceptor, fn: Interceptor) { + const index = this.getInterceptorIndex(id); + if (this._fns[index]) { + this._fns[index] = fn; + return id; + } else { + return false; + } + } + + use(fn: Interceptor) { + this._fns = [...this._fns, fn]; + return this._fns.length - 1; + } +} + +// `createInterceptors()` response, meant for external use as it does not +// expose internals +export interface Middleware { + error: Pick< + Interceptors>, + 'eject' | 'use' + >; + request: Pick>, 'eject' | 'use'>; + response: Pick< + Interceptors>, + 'eject' | 'use' + >; +} + +// do not add `Middleware` as return type so we can use _fns internally +export const createInterceptors = () => ({ + error: new Interceptors>(), + request: new Interceptors>(), + response: new Interceptors>(), +}); + +const defaultQuerySerializer = createQuerySerializer({ + allowReserved: false, + array: { + explode: true, + style: 'form', + }, + object: { + explode: true, + style: 'deepObject', + }, +}); + +const defaultHeaders = { + 'Content-Type': 'application/json', +}; + +export const createConfig = ( + override: Config & T> = {}, +): Config & T> => ({ + ...jsonBodySerializer, + headers: defaultHeaders, + parseAs: 'auto', + querySerializer: defaultQuerySerializer, + ...override, +}); diff --git a/ui/desktop/src/api/core/auth.gen.ts b/ui/desktop/src/api/core/auth.gen.ts new file mode 100644 index 000000000000..f8a73266f934 --- /dev/null +++ b/ui/desktop/src/api/core/auth.gen.ts @@ -0,0 +1,42 @@ +// This file is auto-generated by @hey-api/openapi-ts + +export type AuthToken = string | undefined; + +export interface Auth { + /** + * Which part of the request do we use to send the auth? + * + * @default 'header' + */ + in?: 'header' | 'query' | 'cookie'; + /** + * Header or query parameter name. + * + * @default 'Authorization' + */ + name?: string; + scheme?: 'basic' | 'bearer'; + type: 'apiKey' | 'http'; +} + +export const getAuthToken = async ( + auth: Auth, + callback: ((auth: Auth) => Promise | AuthToken) | AuthToken, +): Promise => { + const token = + typeof callback === 'function' ? await callback(auth) : callback; + + if (!token) { + return; + } + + if (auth.scheme === 'bearer') { + return `Bearer ${token}`; + } + + if (auth.scheme === 'basic') { + return `Basic ${btoa(token)}`; + } + + return token; +}; diff --git a/ui/desktop/src/api/core/bodySerializer.gen.ts b/ui/desktop/src/api/core/bodySerializer.gen.ts new file mode 100644 index 000000000000..49cd8925e3bd --- /dev/null +++ b/ui/desktop/src/api/core/bodySerializer.gen.ts @@ -0,0 +1,92 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { + ArrayStyle, + ObjectStyle, + SerializerOptions, +} from './pathSerializer.gen'; + +export type QuerySerializer = (query: Record) => string; + +export type BodySerializer = (body: any) => any; + +export interface QuerySerializerOptions { + allowReserved?: boolean; + array?: SerializerOptions; + object?: SerializerOptions; +} + +const serializeFormDataPair = ( + data: FormData, + key: string, + value: unknown, +): void => { + if (typeof value === 'string' || value instanceof Blob) { + data.append(key, value); + } else if (value instanceof Date) { + data.append(key, value.toISOString()); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +const serializeUrlSearchParamsPair = ( + data: URLSearchParams, + key: string, + value: unknown, +): void => { + if (typeof value === 'string') { + data.append(key, value); + } else { + data.append(key, JSON.stringify(value)); + } +}; + +export const formDataBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): FormData => { + const data = new FormData(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeFormDataPair(data, key, v)); + } else { + serializeFormDataPair(data, key, value); + } + }); + + return data; + }, +}; + +export const jsonBodySerializer = { + bodySerializer: (body: T): string => + JSON.stringify(body, (_key, value) => + typeof value === 'bigint' ? value.toString() : value, + ), +}; + +export const urlSearchParamsBodySerializer = { + bodySerializer: | Array>>( + body: T, + ): string => { + const data = new URLSearchParams(); + + Object.entries(body).forEach(([key, value]) => { + if (value === undefined || value === null) { + return; + } + if (Array.isArray(value)) { + value.forEach((v) => serializeUrlSearchParamsPair(data, key, v)); + } else { + serializeUrlSearchParamsPair(data, key, value); + } + }); + + return data.toString(); + }, +}; diff --git a/ui/desktop/src/api/core/params.gen.ts b/ui/desktop/src/api/core/params.gen.ts new file mode 100644 index 000000000000..71c88e852b72 --- /dev/null +++ b/ui/desktop/src/api/core/params.gen.ts @@ -0,0 +1,153 @@ +// This file is auto-generated by @hey-api/openapi-ts + +type Slot = 'body' | 'headers' | 'path' | 'query'; + +export type Field = + | { + in: Exclude; + /** + * Field name. This is the name we want the user to see and use. + */ + key: string; + /** + * Field mapped name. This is the name we want to use in the request. + * If omitted, we use the same value as `key`. + */ + map?: string; + } + | { + in: Extract; + /** + * Key isn't required for bodies. + */ + key?: string; + map?: string; + }; + +export interface Fields { + allowExtra?: Partial>; + args?: ReadonlyArray; +} + +export type FieldsConfig = ReadonlyArray; + +const extraPrefixesMap: Record = { + $body_: 'body', + $headers_: 'headers', + $path_: 'path', + $query_: 'query', +}; +const extraPrefixes = Object.entries(extraPrefixesMap); + +type KeyMap = Map< + string, + { + in: Slot; + map?: string; + } +>; + +const buildKeyMap = (fields: FieldsConfig, map?: KeyMap): KeyMap => { + if (!map) { + map = new Map(); + } + + for (const config of fields) { + if ('in' in config) { + if (config.key) { + map.set(config.key, { + in: config.in, + map: config.map, + }); + } + } else if (config.args) { + buildKeyMap(config.args, map); + } + } + + return map; +}; + +interface Params { + body: unknown; + headers: Record; + path: Record; + query: Record; +} + +const stripEmptySlots = (params: Params) => { + for (const [slot, value] of Object.entries(params)) { + if (value && typeof value === 'object' && !Object.keys(value).length) { + delete params[slot as Slot]; + } + } +}; + +export const buildClientParams = ( + args: ReadonlyArray, + fields: FieldsConfig, +) => { + const params: Params = { + body: {}, + headers: {}, + path: {}, + query: {}, + }; + + const map = buildKeyMap(fields); + + let config: FieldsConfig[number] | undefined; + + for (const [index, arg] of args.entries()) { + if (fields[index]) { + config = fields[index]; + } + + if (!config) { + continue; + } + + if ('in' in config) { + if (config.key) { + const field = map.get(config.key)!; + const name = field.map || config.key; + (params[field.in] as Record)[name] = arg; + } else { + params.body = arg; + } + } else { + for (const [key, value] of Object.entries(arg ?? {})) { + const field = map.get(key); + + if (field) { + const name = field.map || key; + (params[field.in] as Record)[name] = value; + } else { + const extra = extraPrefixes.find(([prefix]) => + key.startsWith(prefix), + ); + + if (extra) { + const [prefix, slot] = extra; + (params[slot] as Record)[ + key.slice(prefix.length) + ] = value; + } else { + for (const [slot, allowed] of Object.entries( + config.allowExtra ?? {}, + )) { + if (allowed) { + (params[slot as Slot] as Record)[key] = value; + break; + } + } + } + } + } + } + } + + stripEmptySlots(params); + + return params; +}; diff --git a/ui/desktop/src/api/core/pathSerializer.gen.ts b/ui/desktop/src/api/core/pathSerializer.gen.ts new file mode 100644 index 000000000000..8d9993104743 --- /dev/null +++ b/ui/desktop/src/api/core/pathSerializer.gen.ts @@ -0,0 +1,181 @@ +// This file is auto-generated by @hey-api/openapi-ts + +interface SerializeOptions + extends SerializePrimitiveOptions, + SerializerOptions {} + +interface SerializePrimitiveOptions { + allowReserved?: boolean; + name: string; +} + +export interface SerializerOptions { + /** + * @default true + */ + explode: boolean; + style: T; +} + +export type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited'; +export type ArraySeparatorStyle = ArrayStyle | MatrixStyle; +type MatrixStyle = 'label' | 'matrix' | 'simple'; +export type ObjectStyle = 'form' | 'deepObject'; +type ObjectSeparatorStyle = ObjectStyle | MatrixStyle; + +interface SerializePrimitiveParam extends SerializePrimitiveOptions { + value: string; +} + +export const separatorArrayExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const separatorArrayNoExplode = (style: ArraySeparatorStyle) => { + switch (style) { + case 'form': + return ','; + case 'pipeDelimited': + return '|'; + case 'spaceDelimited': + return '%20'; + default: + return ','; + } +}; + +export const separatorObjectExplode = (style: ObjectSeparatorStyle) => { + switch (style) { + case 'label': + return '.'; + case 'matrix': + return ';'; + case 'simple': + return ','; + default: + return '&'; + } +}; + +export const serializeArrayParam = ({ + allowReserved, + explode, + name, + style, + value, +}: SerializeOptions & { + value: unknown[]; +}) => { + if (!explode) { + const joinedValues = ( + allowReserved ? value : value.map((v) => encodeURIComponent(v as string)) + ).join(separatorArrayNoExplode(style)); + switch (style) { + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + case 'simple': + return joinedValues; + default: + return `${name}=${joinedValues}`; + } + } + + const separator = separatorArrayExplode(style); + const joinedValues = value + .map((v) => { + if (style === 'label' || style === 'simple') { + return allowReserved ? v : encodeURIComponent(v as string); + } + + return serializePrimitiveParam({ + allowReserved, + name, + value: v as string, + }); + }) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; + +export const serializePrimitiveParam = ({ + allowReserved, + name, + value, +}: SerializePrimitiveParam) => { + if (value === undefined || value === null) { + return ''; + } + + if (typeof value === 'object') { + throw new Error( + 'Deeply-nested arrays/objects aren’t supported. Provide your own `querySerializer()` to handle these.', + ); + } + + return `${name}=${allowReserved ? value : encodeURIComponent(value)}`; +}; + +export const serializeObjectParam = ({ + allowReserved, + explode, + name, + style, + value, + valueOnly, +}: SerializeOptions & { + value: Record | Date; + valueOnly?: boolean; +}) => { + if (value instanceof Date) { + return valueOnly ? value.toISOString() : `${name}=${value.toISOString()}`; + } + + if (style !== 'deepObject' && !explode) { + let values: string[] = []; + Object.entries(value).forEach(([key, v]) => { + values = [ + ...values, + key, + allowReserved ? (v as string) : encodeURIComponent(v as string), + ]; + }); + const joinedValues = values.join(','); + switch (style) { + case 'form': + return `${name}=${joinedValues}`; + case 'label': + return `.${joinedValues}`; + case 'matrix': + return `;${name}=${joinedValues}`; + default: + return joinedValues; + } + } + + const separator = separatorObjectExplode(style); + const joinedValues = Object.entries(value) + .map(([key, v]) => + serializePrimitiveParam({ + allowReserved, + name: style === 'deepObject' ? `${name}[${key}]` : key, + value: v as string, + }), + ) + .join(separator); + return style === 'label' || style === 'matrix' + ? separator + joinedValues + : joinedValues; +}; diff --git a/ui/desktop/src/api/core/types.gen.ts b/ui/desktop/src/api/core/types.gen.ts new file mode 100644 index 000000000000..5bfae35c0af1 --- /dev/null +++ b/ui/desktop/src/api/core/types.gen.ts @@ -0,0 +1,120 @@ +// This file is auto-generated by @hey-api/openapi-ts + +import type { Auth, AuthToken } from './auth.gen'; +import type { + BodySerializer, + QuerySerializer, + QuerySerializerOptions, +} from './bodySerializer.gen'; + +export interface Client< + RequestFn = never, + Config = unknown, + MethodFn = never, + BuildUrlFn = never, +> { + /** + * Returns the final request URL. + */ + buildUrl: BuildUrlFn; + connect: MethodFn; + delete: MethodFn; + get: MethodFn; + getConfig: () => Config; + head: MethodFn; + options: MethodFn; + patch: MethodFn; + post: MethodFn; + put: MethodFn; + request: RequestFn; + setConfig: (config: Config) => Config; + trace: MethodFn; +} + +export interface Config { + /** + * Auth token or a function returning auth token. The resolved value will be + * added to the request payload as defined by its `security` array. + */ + auth?: ((auth: Auth) => Promise | AuthToken) | AuthToken; + /** + * A function for serializing request body parameter. By default, + * {@link JSON.stringify()} will be used. + */ + bodySerializer?: BodySerializer | null; + /** + * An object containing any HTTP headers that you want to pre-populate your + * `Headers` object with. + * + * {@link https://developer.mozilla.org/docs/Web/API/Headers/Headers#init See more} + */ + headers?: + | RequestInit['headers'] + | Record< + string, + | string + | number + | boolean + | (string | number | boolean)[] + | null + | undefined + | unknown + >; + /** + * The request method. + * + * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more} + */ + method?: + | 'CONNECT' + | 'DELETE' + | 'GET' + | 'HEAD' + | 'OPTIONS' + | 'PATCH' + | 'POST' + | 'PUT' + | 'TRACE'; + /** + * A function for serializing request query parameters. By default, arrays + * will be exploded in form style, objects will be exploded in deepObject + * style, and reserved characters are percent-encoded. + * + * This method will have no effect if the native `paramsSerializer()` Axios + * API function is used. + * + * {@link https://swagger.io/docs/specification/serialization/#query View examples} + */ + querySerializer?: QuerySerializer | QuerySerializerOptions; + /** + * A function validating request data. This is useful if you want to ensure + * the request conforms to the desired shape, so it can be safely sent to + * the server. + */ + requestValidator?: (data: unknown) => Promise; + /** + * A function transforming response data before it's returned. This is useful + * for post-processing data, e.g. converting ISO strings into Date objects. + */ + responseTransformer?: (data: unknown) => Promise; + /** + * A function validating response data. This is useful if you want to ensure + * the response conforms to the desired shape, so it can be safely passed to + * the transformers and returned to the user. + */ + responseValidator?: (data: unknown) => Promise; +} + +type IsExactlyNeverOrNeverUndefined = [T] extends [never] + ? true + : [T] extends [never | undefined] + ? [undefined] extends [T] + ? false + : true + : false; + +export type OmitNever> = { + [K in keyof T as IsExactlyNeverOrNeverUndefined extends true + ? never + : K]: T[K]; +}; diff --git a/ui/desktop/src/api/sdk.gen.ts b/ui/desktop/src/api/sdk.gen.ts index 703749ff11e5..e5407d98db3d 100644 --- a/ui/desktop/src/api/sdk.gen.ts +++ b/ui/desktop/src/api/sdk.gen.ts @@ -1,7 +1,7 @@ // This file is auto-generated by @hey-api/openapi-ts -import type { Options as ClientOptions, TDataShape, Client } from '@hey-api/client-fetch'; -import type { AddSubRecipesData, AddSubRecipesResponse2, ExtendPromptData, ExtendPromptResponse2, UpdateSessionConfigData, UpdateSessionConfigResponse, GetToolsData, GetToolsResponse, UpdateAgentProviderData, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponse, ReadAllConfigData, ReadAllConfigResponse, BackupConfigData, BackupConfigResponse, CreateCustomProviderData, CreateCustomProviderResponse, RemoveCustomProviderData, RemoveCustomProviderResponse, GetExtensionsData, GetExtensionsResponse, AddExtensionData, AddExtensionResponse, RemoveExtensionData, RemoveExtensionResponse, InitConfigData, InitConfigResponse, UpsertPermissionsData, UpsertPermissionsResponse, ProvidersData, ProvidersResponse2, ReadConfigData, RecoverConfigData, RecoverConfigResponse, RemoveConfigData, RemoveConfigResponse, UpsertConfigData, UpsertConfigResponse, ValidateConfigData, ValidateConfigResponse, ConfirmPermissionData, ManageContextData, ManageContextResponse, CreateRecipeData, CreateRecipeResponse2, DecodeRecipeData, DecodeRecipeResponse2, EncodeRecipeData, EncodeRecipeResponse2, ScanRecipeData, ScanRecipeResponse2, CreateScheduleData, CreateScheduleResponse, DeleteScheduleData, DeleteScheduleResponse, ListSchedulesData, ListSchedulesResponse2, UpdateScheduleData, UpdateScheduleResponse, InspectRunningJobData, InspectRunningJobResponse, KillRunningJobData, PauseScheduleData, PauseScheduleResponse, RunNowHandlerData, RunNowHandlerResponse, SessionsHandlerData, SessionsHandlerResponse, UnpauseScheduleData, UnpauseScheduleResponse, ListSessionsData, ListSessionsResponse, GetSessionHistoryData, GetSessionHistoryResponse } from './types.gen'; +import type { Options as ClientOptions, TDataShape, Client } from './client'; +import type { AddSubRecipesData, AddSubRecipesResponses, AddSubRecipesErrors, ExtendPromptData, ExtendPromptResponses, ExtendPromptErrors, UpdateSessionConfigData, UpdateSessionConfigResponses, UpdateSessionConfigErrors, GetToolsData, GetToolsResponses, GetToolsErrors, UpdateAgentProviderData, UpdateAgentProviderResponses, UpdateAgentProviderErrors, UpdateRouterToolSelectorData, UpdateRouterToolSelectorResponses, UpdateRouterToolSelectorErrors, ReadAllConfigData, ReadAllConfigResponses, BackupConfigData, BackupConfigResponses, BackupConfigErrors, CreateCustomProviderData, CreateCustomProviderResponses, CreateCustomProviderErrors, RemoveCustomProviderData, RemoveCustomProviderResponses, RemoveCustomProviderErrors, GetExtensionsData, GetExtensionsResponses, GetExtensionsErrors, AddExtensionData, AddExtensionResponses, AddExtensionErrors, RemoveExtensionData, RemoveExtensionResponses, RemoveExtensionErrors, InitConfigData, InitConfigResponses, InitConfigErrors, UpsertPermissionsData, UpsertPermissionsResponses, UpsertPermissionsErrors, ProvidersData, ProvidersResponses, ReadConfigData, ReadConfigResponses, ReadConfigErrors, RecoverConfigData, RecoverConfigResponses, RecoverConfigErrors, RemoveConfigData, RemoveConfigResponses, RemoveConfigErrors, UpsertConfigData, UpsertConfigResponses, UpsertConfigErrors, ValidateConfigData, ValidateConfigResponses, ValidateConfigErrors, ConfirmPermissionData, ConfirmPermissionResponses, ConfirmPermissionErrors, ManageContextData, ManageContextResponses, ManageContextErrors, CreateRecipeData, CreateRecipeResponses, CreateRecipeErrors, DecodeRecipeData, DecodeRecipeResponses, DecodeRecipeErrors, EncodeRecipeData, EncodeRecipeResponses, EncodeRecipeErrors, ScanRecipeData, ScanRecipeResponses, CreateScheduleData, CreateScheduleResponses, CreateScheduleErrors, DeleteScheduleData, DeleteScheduleResponses, DeleteScheduleErrors, ListSchedulesData, ListSchedulesResponses, ListSchedulesErrors, UpdateScheduleData, UpdateScheduleResponses, UpdateScheduleErrors, InspectRunningJobData, InspectRunningJobResponses, InspectRunningJobErrors, KillRunningJobData, KillRunningJobResponses, PauseScheduleData, PauseScheduleResponses, PauseScheduleErrors, RunNowHandlerData, RunNowHandlerResponses, RunNowHandlerErrors, SessionsHandlerData, SessionsHandlerResponses, SessionsHandlerErrors, UnpauseScheduleData, UnpauseScheduleResponses, UnpauseScheduleErrors, ListSessionsData, ListSessionsResponses, ListSessionsErrors, GetSessionHistoryData, GetSessionHistoryResponses, GetSessionHistoryErrors } from './types.gen'; import { client as _heyApiClient } from './client.gen'; export type Options = ClientOptions & { @@ -19,210 +19,210 @@ export type Options(options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/agent/add_sub_recipes', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const extendPrompt = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/agent/prompt', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const updateSessionConfig = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/agent/session_config', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const getTools = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/agent/tools', ...options }); }; export const updateAgentProvider = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/agent/update_provider', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const updateRouterToolSelector = (options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? _heyApiClient).post({ url: '/agent/update_router_tool_selector', ...options }); }; export const readAllConfig = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/config', ...options }); }; export const backupConfig = (options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? _heyApiClient).post({ url: '/config/backup', ...options }); }; export const createCustomProvider = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/custom-providers', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const removeCustomProvider = (options: Options) => { - return (options.client ?? _heyApiClient).delete({ + return (options.client ?? _heyApiClient).delete({ url: '/config/custom-providers/{id}', ...options }); }; export const getExtensions = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/config/extensions', ...options }); }; export const addExtension = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/extensions', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const removeExtension = (options: Options) => { - return (options.client ?? _heyApiClient).delete({ + return (options.client ?? _heyApiClient).delete({ url: '/config/extensions/{name}', ...options }); }; export const initConfig = (options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? _heyApiClient).post({ url: '/config/init', ...options }); }; export const upsertPermissions = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/permissions', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const providers = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/config/providers', ...options }); }; export const readConfig = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/read', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const recoverConfig = (options?: Options) => { - return (options?.client ?? _heyApiClient).post({ + return (options?.client ?? _heyApiClient).post({ url: '/config/recover', ...options }); }; export const removeConfig = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/remove', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const upsertConfig = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/config/upsert', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const validateConfig = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/config/validate', ...options }); }; export const confirmPermission = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/confirm', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const manageContext = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/context/manage', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; @@ -231,136 +231,136 @@ export const manageContext = (options: Opt * Create a Recipe configuration from the current session */ export const createRecipe = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/recipes/create', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const decodeRecipe = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/recipes/decode', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const encodeRecipe = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/recipes/encode', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const scanRecipe = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/recipes/scan', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const createSchedule = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/schedule/create', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const deleteSchedule = (options: Options) => { - return (options.client ?? _heyApiClient).delete({ + return (options.client ?? _heyApiClient).delete({ url: '/schedule/delete/{id}', ...options }); }; export const listSchedules = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/schedule/list', ...options }); }; export const updateSchedule = (options: Options) => { - return (options.client ?? _heyApiClient).put({ + return (options.client ?? _heyApiClient).put({ url: '/schedule/{id}', ...options, headers: { 'Content-Type': 'application/json', - ...options?.headers + ...options.headers } }); }; export const inspectRunningJob = (options: Options) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? _heyApiClient).get({ url: '/schedule/{id}/inspect', ...options }); }; export const killRunningJob = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/schedule/{id}/kill', ...options }); }; export const pauseSchedule = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/schedule/{id}/pause', ...options }); }; export const runNowHandler = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/schedule/{id}/run_now', ...options }); }; export const sessionsHandler = (options: Options) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? _heyApiClient).get({ url: '/schedule/{id}/sessions', ...options }); }; export const unpauseSchedule = (options: Options) => { - return (options.client ?? _heyApiClient).post({ + return (options.client ?? _heyApiClient).post({ url: '/schedule/{id}/unpause', ...options }); }; export const listSessions = (options?: Options) => { - return (options?.client ?? _heyApiClient).get({ + return (options?.client ?? _heyApiClient).get({ url: '/sessions', ...options }); }; export const getSessionHistory = (options: Options) => { - return (options.client ?? _heyApiClient).get({ + return (options.client ?? _heyApiClient).get({ url: '/sessions/{session_id}', ...options }); diff --git a/ui/desktop/src/api/types.gen.ts b/ui/desktop/src/api/types.gen.ts index abae88556db8..a400fffd14d0 100644 --- a/ui/desktop/src/api/types.gen.ts +++ b/ui/desktop/src/api/types.gen.ts @@ -59,7 +59,9 @@ export type ConfigKeyQuery = { }; export type ConfigResponse = { - config: {}; + config: { + [key: string]: unknown; + }; }; export type Content = RawTextContent | RawImageContent | RawEmbeddedResource | Annotated; From f334328a56bf6c7d3478a3abaab4a71ae8ae77c5 Mon Sep 17 00:00:00 2001 From: Spence Date: Tue, 19 Aug 2025 22:33:50 -0400 Subject: [PATCH 20/20] Add scrollable layout to onboarding page (#4206) --- ui/desktop/src/components/ProviderGuard.tsx | 38 ++++++++++++--------- 1 file changed, 21 insertions(+), 17 deletions(-) diff --git a/ui/desktop/src/components/ProviderGuard.tsx b/ui/desktop/src/components/ProviderGuard.tsx index 8a4b1b703042..761c000904be 100644 --- a/ui/desktop/src/components/ProviderGuard.tsx +++ b/ui/desktop/src/components/ProviderGuard.tsx @@ -200,25 +200,27 @@ export default function ProviderGuard({ children }: ProviderGuardProps) { if (showFirstTimeSetup) { return ( -
-
- {/* Header section - same width as buttons, left aligned */} -
-
-
- +
+
+
+
+ {/* Header section - same width as buttons, left aligned */} +
+
+
+ +
+

+ Welcome to Goose +

+
+

+ Since it's your first time here, let's get your set you with a provider so we can make incredible work together. +

-

- Welcome to Goose -

-
-

- Since it's your first time here, let's get your set you with a provider so we can make incredible work together. -

-
- {/* Setup options - same width container */} -
+ {/* Setup options - same width container */} +
{/* Primary OpenRouter Card with subtle shimmer - wrapped for badge positioning */}
{/* Recommended badge - positioned relative to wrapper */} @@ -313,6 +315,8 @@ export default function ProviderGuard({ children }: ProviderGuardProps) {

+
+