UI Recipe/Custom Agents (#2119)

Co-authored-by: Kalvin Chau <kalvin@block.xyz>
This commit is contained in:
Zaki Ali
2025-04-11 13:28:29 -07:00
committed by GitHub
parent b030f845ce
commit 454e4a47f4
21 changed files with 1321 additions and 246 deletions

View File

@@ -4,6 +4,7 @@ pub mod config_management;
pub mod configs;
pub mod extension;
pub mod health;
pub mod recipe;
pub mod reply;
pub mod session;
pub mod utils;
@@ -18,5 +19,6 @@ pub fn configure(state: crate::state::AppState) -> Router {
.merge(extension::routes(state.clone()))
.merge(configs::routes(state.clone()))
.merge(config_management::routes(state.clone()))
.merge(recipe::routes(state.clone()))
.merge(session::routes(state))
}

View File

@@ -0,0 +1,89 @@
use axum::{extract::State, http::StatusCode, routing::post, Json, Router};
use goose::message::Message;
use goose::recipe::Recipe;
use serde::{Deserialize, Serialize};
use crate::state::AppState;
#[derive(Debug, Deserialize)]
pub struct CreateRecipeRequest {
messages: Vec<Message>,
// Required metadata
title: String,
description: String,
// Optional fields
#[serde(default)]
activities: Option<Vec<String>>,
#[serde(default)]
author: Option<AuthorRequest>,
}
#[derive(Debug, Deserialize)]
pub struct AuthorRequest {
#[serde(default)]
contact: Option<String>,
#[serde(default)]
metadata: Option<String>,
}
#[derive(Debug, Serialize)]
pub struct CreateRecipeResponse {
recipe: Option<Recipe>,
error: Option<String>,
}
/// Create a Recipe configuration from the current state of an agent
async fn create_recipe(
State(state): State<AppState>,
Json(request): Json<CreateRecipeRequest>,
) -> Result<Json<CreateRecipeResponse>, (StatusCode, Json<CreateRecipeResponse>)> {
let agent = state.agent.read().await;
let agent = agent.as_ref().ok_or_else(|| {
let error_response = CreateRecipeResponse {
recipe: None,
error: Some("Agent not initialized".to_string()),
};
(StatusCode::PRECONDITION_REQUIRED, Json(error_response))
})?;
// Create base recipe from agent state and messages
let recipe_result = agent.create_recipe(request.messages).await;
match recipe_result {
Ok(mut recipe) => {
// Update with user-provided metadata
recipe.title = request.title;
recipe.description = request.description;
if request.activities.is_some() {
recipe.activities = request.activities
};
// Add author if provided
if let Some(author_req) = request.author {
recipe.author = Some(goose::recipe::Author {
contact: author_req.contact,
metadata: author_req.metadata,
});
}
Ok(Json(CreateRecipeResponse {
recipe: Some(recipe),
error: None,
}))
}
Err(e) => {
// Return 400 Bad Request with error message
let error_response = CreateRecipeResponse {
recipe: None,
error: Some(e.to_string()),
};
Err((StatusCode::BAD_REQUEST, Json(error_response)))
}
}
}
pub fn routes(state: AppState) -> Router {
Router::new()
.route("/recipe/create", post(create_recipe))
.with_state(state)
}

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useEffect, useRef, useState, useCallback } from 'react';
import { IpcRendererEvent } from 'electron';
import { addExtensionFromDeepLink } from './extensions';
import { openSharedSessionFromDeepLink } from './sessionLinks';
@@ -27,7 +27,9 @@ import ConfigureProvidersView from './components/settings/providers/ConfigurePro
import SessionsView from './components/sessions/SessionsView';
import SharedSessionView from './components/sessions/SharedSessionView';
import ProviderSettings from './components/settings_v2/providers/ProviderSettingsPage';
import RecipeEditor from './components/RecipeEditor';
import { useChat } from './hooks/useChat';
import { addExtension as addExtensionDirect, FullExtensionConfig } from './extensions';
import 'react-toastify/dist/ReactToastify.css';
import { useConfig, MalformedConfigError } from './components/ConfigContext';
@@ -46,7 +48,8 @@ export type View =
| 'settingsV2'
| 'sessions'
| 'sharedSession'
| 'loading';
| 'loading'
| 'recipeEditor';
export type ViewOptions =
| SettingsViewOptions
@@ -59,16 +62,47 @@ export type ViewConfig = {
viewOptions?: ViewOptions;
};
const getInitialView = (): ViewConfig => {
const urlParams = new URLSearchParams(window.location.search);
const viewFromUrl = urlParams.get('view');
const windowConfig = window.electron.getConfig();
if (viewFromUrl === 'recipeEditor' && windowConfig?.recipeConfig) {
return {
view: 'recipeEditor',
viewOptions: {
config: windowConfig.recipeConfig,
},
};
}
// Any other URL-specified view
if (viewFromUrl) {
return {
view: viewFromUrl as View,
viewOptions: {},
};
}
// Default case
return {
view: 'welcome',
viewOptions: {},
};
};
export default function App() {
const [fatalError, setFatalError] = useState<string | null>(null);
const [modalVisible, setModalVisible] = useState(false);
const [pendingLink, setPendingLink] = useState<string | null>(null);
const [modalMessage, setModalMessage] = useState<string>('');
const [{ view, viewOptions }, setInternalView] = useState<ViewConfig>({
view: 'loading',
viewOptions: {},
});
const { getExtensions, addExtension, read } = useConfig();
const [{ view, viewOptions }, setInternalView] = useState<ViewConfig>(getInitialView());
const {
getExtensions,
addExtension: addExtensionToConfig,
disableAllExtensions,
read,
} = useConfig();
const initAttemptedRef = useRef(false);
// Utility function to extract the command from the link
@@ -90,7 +124,153 @@ export default function App() {
setInternalView({ view, viewOptions });
};
// Single initialization effect that handles both v1 and v2 settings
const disableAllStoredExtensions = () => {
const userSettingsStr = localStorage.getItem('user_settings');
if (!userSettingsStr) return;
try {
const userSettings = JSON.parse(userSettingsStr);
// Store original state before modifying
localStorage.setItem('user_settings_backup', userSettingsStr);
console.log('Backing up user_settings');
// Disable all extensions
userSettings.extensions = userSettings.extensions.map((ext) => ({
...ext,
enabled: false,
}));
localStorage.setItem('user_settings', JSON.stringify(userSettings));
console.log('Disabled all stored extensions');
window.electron.emit('settings-updated');
} catch (error) {
console.error('Error disabling stored extensions:', error);
}
};
// Function to restore original extension states for new non-recipe windows
const restoreOriginalExtensionStates = () => {
const backupStr = localStorage.getItem('user_settings_backup');
if (backupStr) {
localStorage.setItem('user_settings', backupStr);
console.log('Restored original extension states');
}
};
const updateUserSettingsWithConfig = (extensions: FullExtensionConfig[]) => {
try {
const userSettingsStr = localStorage.getItem('user_settings');
const userSettings = userSettingsStr ? JSON.parse(userSettingsStr) : { extensions: [] };
// For each extension in the passed in config
extensions.forEach((newExtension) => {
// Find if this extension already exists
const existingIndex = userSettings.extensions.findIndex(
(ext) => ext.id === newExtension.id
);
if (existingIndex !== -1) {
// Extension exists - just set its enabled to true
userSettings.extensions[existingIndex].enabled = true;
} else {
// Extension is new - add it to the array
userSettings.extensions.push({
...newExtension,
enabled: true,
});
}
});
localStorage.setItem('user_settings', JSON.stringify(userSettings));
console.log('Updated user settings with new/enabled extensions:', userSettings.extensions);
// Notify any listeners (like the settings page) that settings have changed
window.electron.emit('settings-updated');
} catch (error) {
console.error('Error updating user settings:', error);
}
};
const enableRecipeConfigExtensions = async (extensions: FullExtensionConfig[]) => {
if (!extensions?.length) {
console.log('No extensions to enable from bot config');
return;
}
console.log(`Enabling ${extensions.length} extensions from bot config:`, extensions);
disableAllStoredExtensions();
// Wait for initial server readiness
await new Promise((resolve) => setTimeout(resolve, 2000));
for (const extension of extensions) {
try {
console.log(`Enabling extension: ${extension.name}`);
const extensionConfig = {
...extension,
enabled: true,
};
// Try to add the extension
const response = await addExtensionDirect(extensionConfig, false);
if (!response.ok) {
console.error(
`Failed to enable extension ${extension.name}: Server returned ${response.status}`
);
// If it's a 428, retry once
if (response.status === 428) {
console.log('Server not ready, waiting and will retry...');
await new Promise((resolve) => setTimeout(resolve, 2000));
try {
await addExtensionDirect(extensionConfig, true);
console.log(`Successfully enabled extension ${extension.name} on retry`);
} catch (retryError) {
console.error(`Failed to enable extension ${extension.name} on retry:`, retryError);
}
}
continue;
}
updateUserSettingsWithConfig(extensions);
console.log(`Successfully enabled extension: ${extension.name}`);
} catch (error) {
console.error(`Failed to enable extension ${extension.name}:`, error);
}
}
console.log('Finished enabling bot config extensions');
};
const enableRecipeConfigExtensionsV2 = useCallback(
async (extensions: FullExtensionConfig[]) => {
if (!extensions?.length) {
console.log('No extensions to enable from bot config');
return;
}
try {
await disableAllExtensions();
console.log('Disabled all existing extensions');
for (const extension of extensions) {
try {
console.log('Enabling extension: ${extension.name}');
await addExtensionToConfig(extension.name, extension, true);
} catch (error) {
console.error(`Failed to enable extension ${extension.name}:`, error);
}
}
} catch (error) {
console.error('Failed to enable bot extensions');
}
console.log('Finished enabling bot config extensions');
},
[disableAllExtensions, addExtensionToConfig]
);
// settings v2 initialization
useEffect(() => {
if (!settingsV2Enabled) {
return;
@@ -105,6 +285,27 @@ export default function App() {
console.log(`Initializing app with settings v2`);
const urlParams = new URLSearchParams(window.location.search);
const viewType = urlParams.get('view');
const recipeConfig = window.appConfig.get('recipeConfig');
// Handle bot config extensions first
if (recipeConfig?.extensions?.length > 0 && viewType != 'recipeEditor') {
console.log('Found extensions in bot config:', recipeConfig.extensions);
enableRecipeConfigExtensionsV2(recipeConfig.extensions);
}
// If we have a specific view type in the URL, use that and skip provider detection
if (viewType) {
if (viewType === 'recipeEditor' && recipeConfig) {
console.log('Setting view to recipeEditor with config:', recipeConfig);
setView('recipeEditor', { config: recipeConfig });
} else {
setView(viewType as View);
}
return;
}
const initializeApp = async () => {
try {
// Initialize config first
@@ -121,7 +322,7 @@ export default function App() {
try {
await initializeSystem(provider, model, {
getExtensions,
addExtension,
addExtensionToConfig,
});
} catch (error) {
console.error('Error in initialization:', error);
@@ -153,7 +354,7 @@ export default function App() {
console.error('Unhandled error in initialization:', error);
setFatalError(`${error instanceof Error ? error.message : 'Unknown error'}`);
});
}, [read, getExtensions, addExtension]);
}, [read, getExtensions, addExtensionToConfig, enableRecipeConfigExtensionsV2]);
const [isGoosehintsModalOpen, setIsGoosehintsModalOpen] = useState(false);
const [isLoadingSession, setIsLoadingSession] = useState(false);
@@ -243,6 +444,24 @@ export default function App() {
setView(newView);
};
// Get initial view and config
const urlParams = new URLSearchParams(window.location.search);
const viewFromUrl = urlParams.get('view');
if (viewFromUrl) {
// Get the config from the electron window config
const windowConfig = window.electron.getConfig();
if (viewFromUrl === 'recipeEditor') {
const initialViewOptions = {
recipeConfig: windowConfig?.recipeConfig,
view: viewFromUrl,
};
setView(viewFromUrl, initialViewOptions);
} else {
setView(viewFromUrl);
}
}
window.electron.on('set-view', handleSetView);
return () => window.electron.off('set-view', handleSetView);
}, []);
@@ -250,7 +469,7 @@ export default function App() {
// Add cleanup for session states when view changes
useEffect(() => {
console.log(`View changed to: ${view}`);
if (view !== 'chat') {
if (view !== 'chat' && view !== 'recipeEditor') {
console.log('Not in chat view, clearing loading session state');
setIsLoadingSession(false);
}
@@ -291,7 +510,7 @@ export default function App() {
setModalVisible(false); // Dismiss modal immediately
try {
if (settingsV2Enabled) {
await addExtensionFromDeepLinkV2(pendingLink, addExtension, setView);
await addExtensionFromDeepLinkV2(pendingLink, addExtensionToConfig, setView);
} else {
await addExtensionFromDeepLink(pendingLink, setView);
}
@@ -318,12 +537,40 @@ export default function App() {
const { addRecentModel } = useRecentModels(); // TODO: remove
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
const viewType = urlParams.get('view');
const recipeConfig = window.appConfig.get('recipeConfig');
if (settingsV2Enabled) {
return;
}
console.log(`Initializing app with settings v1`);
// Handle bot config extensions first
if (recipeConfig?.extensions?.length > 0 && viewType != 'recipeEditor') {
console.log('Found extensions in bot config:', recipeConfig.extensions);
enableRecipeConfigExtensions(recipeConfig.extensions);
}
// If we have a specific view type in the URL, use that and skip provider detection
if (viewType) {
if (viewType === 'recipeEditor' && recipeConfig) {
console.log('Setting view to recipeEditor with config:', recipeConfig);
setView('recipeEditor', { config: recipeConfig });
} else {
setView(viewType as View);
}
return;
}
// if not in any of the states above (in a regular chat)
if (!recipeConfig) {
restoreOriginalExtensionStates();
}
console.log(`Initializing app with settings v1`);
// Attempt to detect config for a stored provider
const detectStoredProvider = () => {
try {
@@ -510,6 +757,27 @@ export default function App() {
}}
/>
)}
{view === 'recipeEditor' && (
<RecipeEditor
key={viewOptions?.config ? 'with-config' : 'no-config'}
config={viewOptions?.config || window.electron.getConfig().recipeConfig}
onClose={() => setView('chat')}
setView={setView}
onSave={(config) => {
console.log('Saving recipe config:', config);
window.electron.createChatWindow(
undefined,
undefined,
undefined,
undefined,
config,
'recipeEditor',
{ config }
);
setView('chat');
}}
/>
)}
</div>
</div>
{isGoosehintsModalOpen && (

View File

@@ -1,7 +0,0 @@
/**
* Bot configuration interface
*/
export interface BotConfig {
instructions: string;
activities: string[] | null;
}

View File

@@ -0,0 +1,31 @@
import React from 'react';
interface AgentHeaderProps {
title: string;
profileInfo?: string;
onChangeProfile?: () => void;
}
export function AgentHeader({ title, profileInfo, onChangeProfile }: AgentHeaderProps) {
return (
<div className="flex items-center justify-between px-4 py-2 border-b border-borderSubtle">
<div className="flex items-center">
<span className="w-2 h-2 rounded-full bg-blockTeal mr-2" />
<span className="text-sm">
<span className="text-textSubtle">Agent</span>{' '}
<span className="text-textStandard">{title}</span>
</span>
</div>
{profileInfo && (
<div className="flex items-center text-sm">
<span className="text-textSubtle">{profileInfo}</span>
{onChangeProfile && (
<button onClick={onChangeProfile} className="ml-2 text-blockTeal hover:underline">
change profile
</button>
)}
</div>
)}
</div>
);
}

View File

@@ -12,10 +12,13 @@ import { ScrollArea, ScrollAreaHandle } from './ui/scroll-area';
import UserMessage from './UserMessage';
import Splash from './Splash';
import { SearchView } from './conversation/SearchView';
import { DeepLinkModal } from './ui/DeepLinkModal';
import { createRecipe } from '../recipe';
import { AgentHeader } from './AgentHeader';
import LayingEggLoader from './LayingEggLoader';
// import { configureRecipeExtensions } from '../utils/recipeExtensions';
import 'react-toastify/dist/ReactToastify.css';
import { useMessageStream } from '../hooks/useMessageStream';
import { BotConfig } from '../botConfig';
import { Recipe } from '../recipe';
import {
Message,
createUserMessage,
@@ -24,7 +27,6 @@ import {
ToolRequestMessageContent,
ToolResponseMessageContent,
ToolConfirmationRequestMessageContent,
getTextContent,
EnableExtensionRequestMessageContent,
} from '../types/message';
@@ -37,14 +39,6 @@ export interface ChatType {
messages: Message[];
}
interface GeneratedBotConfig {
id: string;
name: string;
description: string;
instructions: string;
activities: string[];
}
// Helper function to determine if a message is a user message
const isUserMessage = (message: Message): boolean => {
if (message.role === 'assistant') {
@@ -75,13 +69,11 @@ export default function ChatView({
const [hasMessages, setHasMessages] = useState(false);
const [lastInteractionTime, setLastInteractionTime] = useState<number>(Date.now());
const [showGame, setShowGame] = useState(false);
const [waitingForAgentResponse, setWaitingForAgentResponse] = useState(false);
const [showShareableBotModal, setshowShareableBotModal] = useState(false);
const [generatedBotConfig, setGeneratedBotConfig] = useState<GeneratedBotConfig | null>(null);
const [isGeneratingRecipe, setIsGeneratingRecipe] = useState(false);
const scrollRef = useRef<ScrollAreaHandle>(null);
// Get botConfig directly from appConfig
const botConfig = window.appConfig.get('botConfig') as BotConfig | null;
const botConfig = window.appConfig.get('botConfig') as Recipe | null;
const {
messages,
@@ -126,33 +118,50 @@ export default function ChatView({
// Listen for make-agent-from-chat event
useEffect(() => {
const handleMakeAgent = async () => {
window.electron.logInfo('Making agent from chat...');
window.electron.logInfo('Making recipe from chat...');
setIsGeneratingRecipe(true);
// Log all messages for now
window.electron.logInfo('Current messages:');
chat.messages.forEach((message, index) => {
const role = isUserMessage(message) ? 'user' : 'assistant';
const content = isUserMessage(message) ? message.text : getTextContent(message);
window.electron.logInfo(`Message ${index} (${role}): ${content}`);
});
try {
// Create recipe directly from chat messages
const createRecipeRequest = {
messages: messages,
title: 'Custom Recipe',
description: 'Created from chat session',
};
// Inject a question into the chat to generate instructions
const instructionsPrompt =
'Based on our conversation so far, could you create:\n' +
"1. A concise set of instructions (1-2 paragraphs) that describe what you've been helping with. Pay special attention if any output styles or formats are requested (and make it clear), and note any non standard tools used or required.\n" +
'2. A list of 3-5 example activities (as a few words each at most) that would be relevant to this topic\n\n' +
"Format your response with clear headings for 'Instructions:' and 'Activities:' sections." +
'For example, perhaps we have been discussing fruit and you might write:\n\n' +
'Instructions:\nUsing web searches we find pictures of fruit, and always check what language to reply in.' +
'Activities:\nShow pics of apples, say a random fruit, share a fruit fact';
const response = await createRecipe(createRecipeRequest);
// Set waiting state to true before adding the prompt
setWaitingForAgentResponse(true);
if (response.error) {
throw new Error(`Failed to create recipe: ${response.error}`);
}
// Add the prompt as a user message
append(createUserMessage(instructionsPrompt));
window.electron.logInfo('Created recipe:');
window.electron.logInfo(JSON.stringify(response.recipe, null, 2));
window.electron.logInfo('Injected instructions prompt into chat');
// Create a new window for the recipe editor
console.log('Opening recipe editor with config:', response.recipe);
// First, verify the recipe data
if (!response.recipe || !response.recipe.title) {
throw new Error('Invalid recipe data received');
}
window.electron.createChatWindow(
undefined, // query
undefined, // dir
undefined, // version
undefined, // resumeSessionId
response.recipe, // recipe config
'recipeEditor' // view type
);
window.electron.logInfo('Opening recipe editor window');
} catch (error) {
window.electron.logInfo('Failed to create recipe:');
window.electron.logInfo(error.message);
} finally {
setIsGeneratingRecipe(false);
}
};
window.addEventListener('make-agent-from-chat', handleMakeAgent);
@@ -160,74 +169,9 @@ export default function ChatView({
return () => {
window.removeEventListener('make-agent-from-chat', handleMakeAgent);
};
}, [append, chat.messages, setWaitingForAgentResponse]);
// Listen for new messages and process agent response
useEffect(() => {
// Only process if we're waiting for an agent response
if (!waitingForAgentResponse || messages.length === 0) {
return;
}
// Get the last message
const lastMessage = messages[messages.length - 1];
// Check if it's an assistant message (response to our prompt)
if (lastMessage.role === 'assistant') {
// Extract the content
const content = getTextContent(lastMessage);
// Process the agent's response
if (content) {
window.electron.logInfo('Received agent response:');
window.electron.logInfo(content);
// Parse the response to extract instructions and activities
const instructionsMatch = content.match(/Instructions:(.*?)(?=Activities:|$)/s);
const activitiesMatch = content.match(/Activities:(.*?)$/s);
const instructions = instructionsMatch ? instructionsMatch[1].trim() : '';
const activitiesText = activitiesMatch ? activitiesMatch[1].trim() : '';
// Parse activities into an array
const activities = activitiesText
.split(/\n+/)
.map((line) => line.replace(/^[•\-*\d]+\.?\s*/, '').trim())
.filter((activity) => activity.length > 0);
// Create a bot config object
const generatedConfig = {
id: `bot-${Date.now()}`,
name: 'Custom Bot',
description: 'Bot created from chat',
instructions: instructions,
activities: activities,
};
window.electron.logInfo('Extracted bot config:');
window.electron.logInfo(JSON.stringify(generatedConfig, null, 2));
// Store the generated bot config
setGeneratedBotConfig(generatedConfig);
// Show the modal with the generated bot config
setshowShareableBotModal(true);
window.electron.logInfo('Generated bot config for agent creation');
// Reset waiting state
setWaitingForAgentResponse(false);
}
}
}, [messages, waitingForAgentResponse, setshowShareableBotModal, setGeneratedBotConfig]);
// Leaving these in for easy debugging of different message states
// One message with a tool call and no text content
// const messages = [{"role":"assistant","created":1742484893,"content":[{"type":"toolRequest","id":"call_udVcu3crnFdx2k5FzlAjk5dI","toolCall":{"status":"success","value":{"name":"developer__text_editor","arguments":{"command":"write","file_text":"Hello, this is a test file.\nLet's see if this works properly.","path":"/Users/alexhancock/Development/testfile.txt"}}}}]}];
// One message with text content and tool calls
// const messages = [{"role":"assistant","created":1742484388,"content":[{"type":"text","text":"Sure, let's break this down into two steps:\n\n1. **Write content to a `.txt` file.**\n2. **Read the content from the `.txt` file.**\n\nLet's start by writing some example content to a `.txt` file. I'll create a file named `example.txt` and write a sample sentence into it. Then I'll read the content back. \n\n### Sample Content\nWe'll write the following content into the `example.txt` file:\n\n```\nHello World! This is an example text file.\n```\n\nLet's proceed with this task."},{"type":"toolRequest","id":"call_CmvAsxMxiWVKZvONZvnz4QCE","toolCall":{"status":"success","value":{"name":"developer__text_editor","arguments":{"command":"write","file_text":"Hello World! This is an example text file.","path":"/Users/alexhancock/Development/example.txt"}}}}]}];
}, [messages]);
// do we need append here?
// }, [append, chat.messages]);
// Update chat messages when they change and save to sessionStorage
useEffect(() => {
@@ -417,15 +361,30 @@ export default function ChatView({
return (
<div className="flex flex-col w-full h-screen items-center justify-center">
{/* Loader when generating recipe */}
{isGeneratingRecipe && <LayingEggLoader />}
<div className="relative flex items-center h-[36px] w-full">
<MoreMenuLayout setView={setView} setIsGoosehintsModalOpen={setIsGoosehintsModalOpen} />
</div>
<Card className="flex flex-col flex-1 rounded-none h-[calc(100vh-95px)] w-full bg-bgApp mt-0 border-none relative">
{botConfig?.title && messages.length > 0 && (
<AgentHeader
title={botConfig.title}
profileInfo={
botConfig.profile ? `${botConfig.profile} - ${botConfig.mcps || 12} MCPs` : undefined
}
onChangeProfile={() => {
// Handle profile change
console.log('Change profile clicked');
}}
/>
)}
{messages.length === 0 ? (
<Splash
append={(text) => append(createUserMessage(text))}
activities={botConfig?.activities || null}
activities={Array.isArray(botConfig?.activities) ? botConfig.activities : null}
title={botConfig?.title}
/>
) : (
<ScrollArea ref={scrollRef} className="flex-1" autoScroll>
@@ -490,21 +449,6 @@ export default function ChatView({
</Card>
{showGame && <FlappyGoose onClose={() => setShowGame(false)} />}
{/* Deep Link Modal */}
{showShareableBotModal && generatedBotConfig && (
<DeepLinkModal
botConfig={generatedBotConfig}
onClose={() => {
setshowShareableBotModal(false);
setGeneratedBotConfig(null);
}}
onOpen={() => {
setshowShareableBotModal(false);
setGeneratedBotConfig(null);
}}
/>
)}
</div>
);
}

View File

@@ -49,6 +49,8 @@ interface ConfigContextType {
removeExtension: (name: string) => Promise<void>;
getProviders: (b: boolean) => Promise<ProviderDetails[]>;
getExtensions: (b: boolean) => Promise<FixedExtensionEntry[]>;
disableAllExtensions: () => Promise<void>;
enableBotExtensions: (extensions: ExtensionConfig[]) => Promise<void>;
}
interface ConfigProviderProps {
@@ -204,8 +206,25 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
})();
}, []);
const contextValue = useMemo(
() => ({
const contextValue = useMemo(() => {
const disableAllExtensions = async () => {
const currentExtensions = await getExtensions(true);
for (const ext of currentExtensions) {
if (ext.enabled) {
await addExtension(ext.name, ext, false);
}
}
await reloadConfig();
};
const enableBotExtensions = async (extensions: ExtensionConfig[]) => {
for (const ext of extensions) {
await addExtension(ext.name, ext, true);
}
await reloadConfig();
};
return {
config,
providersList,
extensionsList,
@@ -217,8 +236,10 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
toggleExtension,
getProviders,
getExtensions,
}),
[
disableAllExtensions,
enableBotExtensions,
};
}, [
config,
providersList,
extensionsList,
@@ -230,8 +251,8 @@ export const ConfigProvider: React.FC<ConfigProviderProps> = ({ children }) => {
toggleExtension,
getProviders,
getExtensions,
]
);
reloadConfig,
]);
return <ConfigContext.Provider value={contextValue}>{children}</ConfigContext.Provider>;
};

View File

@@ -0,0 +1,30 @@
import React, { useEffect, useState } from 'react';
import { Geese } from './icons/Geese';
export default function LayingEggLoader() {
const [dots, setDots] = useState('');
useEffect(() => {
const interval = setInterval(() => {
setDots((prev) => (prev.length >= 3 ? '' : prev + '.'));
}, 500);
return () => clearInterval(interval);
}, []);
return (
<div className="fixed inset-0 flex items-center justify-center z-50 bg-bgApp">
<div className="flex flex-col items-center max-w-3xl w-full px-6 pt-10">
<div className="w-16 h-16 bg-bgApp rounded-full flex items-center justify-center mb-4">
<Geese className="w-12 h-12 text-iconProminent" />
</div>
<h1 className="text-2xl font-medium text-center mb-2 text-textProminent">
Laying an egg{dots}
</h1>
<p className="text-textSubtle text-center text-sm">
Please wait while we process your request
</p>
</div>
</div>
);
}

View File

@@ -0,0 +1,377 @@
import React, { useState, useEffect } from 'react';
import { Recipe } from '../recipe';
import { Buffer } from 'buffer';
import { FullExtensionConfig } from '../extensions';
import { ChevronRight } from './icons/ChevronRight';
import Back from './icons/Back';
import { Bars } from './icons/Bars';
import { Geese } from './icons/Geese';
import Copy from './icons/Copy';
import { useConfig } from '../components/ConfigContext';
import { settingsV2Enabled } from '../flags';
interface RecipeEditorProps {
config?: Recipe;
}
// Function to generate a deep link from a recipe
function generateDeepLink(recipe: Recipe): string {
const configBase64 = Buffer.from(JSON.stringify(recipe)).toString('base64');
return `goose://recipe?config=${configBase64}`;
}
export default function RecipeEditor({ config }: RecipeEditorProps) {
const { getExtensions } = useConfig();
const [recipeConfig] = useState<Recipe | undefined>(config);
const [title, setTitle] = useState(config?.title || '');
const [description, setDescription] = useState(config?.description || '');
const [instructions, setInstructions] = useState(config?.instructions || '');
const [activities, setActivities] = useState<string[]>(config?.activities || []);
const [availableExtensions, setAvailableExtensions] = useState<FullExtensionConfig[]>([]);
const [selectedExtensions, setSelectedExtensions] = useState<string[]>(
config?.extensions?.map((e) => e.id) || []
);
const [newActivity, setNewActivity] = useState('');
// Section visibility state
const [activeSection, setActiveSection] = useState<
'none' | 'activities' | 'instructions' | 'extensions'
>('none');
// Load extensions
useEffect(() => {
const loadExtensions = async () => {
if (settingsV2Enabled) {
try {
const extensions = await getExtensions(false); // force refresh to get latest
console.log('extensions {}', extensions);
setAvailableExtensions(extensions || []);
} catch (error) {
console.error('Failed to load extensions:', error);
}
} else {
const userSettingsStr = localStorage.getItem('user_settings');
if (userSettingsStr) {
const userSettings = JSON.parse(userSettingsStr);
setAvailableExtensions(userSettings.extensions || []);
}
}
};
loadExtensions();
// Intentionally omitting getExtensions from deps to avoid refresh loops
// eslint-disable-next-line
}, []);
const handleExtensionToggle = (id: string) => {
console.log('Toggling extension:', id);
setSelectedExtensions((prev) => {
const isSelected = prev.includes(id);
const newState = isSelected ? prev.filter((extId) => extId !== id) : [...prev, id];
return newState;
});
};
const handleAddActivity = () => {
if (newActivity.trim()) {
setActivities((prev) => [...prev, newActivity.trim()]);
setNewActivity('');
}
};
const handleRemoveActivity = (activity: string) => {
setActivities((prev) => prev.filter((a) => a !== activity));
};
const getCurrentConfig = (): Recipe => {
console.log('Creating config with:', {
selectedExtensions,
availableExtensions,
recipeConfig,
});
const config = {
...recipeConfig,
title,
description,
instructions,
activities,
extensions: selectedExtensions
.map((name) => {
const extension = availableExtensions.find((e) => e.name === name);
console.log('Looking for extension:', name, 'Found:', extension);
if (!extension) return null;
// Create a clean copy of the extension configuration
const cleanExtension = { ...extension };
delete cleanExtension.enabled;
// If the extension has env_keys, preserve keys but clear values
if (cleanExtension.env_keys) {
cleanExtension.env_keys = Object.fromEntries(
Object.keys(cleanExtension.env_keys).map((key) => [key, ''])
);
}
return cleanExtension;
})
.filter(Boolean) as FullExtensionConfig[],
};
console.log('Final config extensions:', config.extensions);
return config;
};
const deeplink = generateDeepLink(getCurrentConfig());
// Render expanded section content
const renderSectionContent = () => {
switch (activeSection) {
case 'activities':
return (
<div className="p-6 pt-10">
<button onClick={() => setActiveSection('none')} className="mb-6">
<Back className="w-6 h-6 text-iconProminent" />
</button>
<div className="py-2">
<Bars className="w-6 h-6 text-iconSubtle" />
</div>
<div className="mb-8 mt-6">
<h2 className="text-2xl font-medium mb-2 text-textProminent">Activities</h2>
<p className="text-textSubtle">
The top-line prompts and activities that will display within your goose home page.
</p>
</div>
<div className="space-y-4">
<div className="flex flex-wrap gap-3">
{activities.map((activity, index) => (
<div
key={index}
className="inline-flex items-center bg-bgApp border-2 border-borderSubtle rounded-full px-4 py-2 text-sm text-textStandard"
title={activity.length > 100 ? activity : undefined}
>
<span>{activity.length > 100 ? activity.slice(0, 100) + '...' : activity}</span>
<button
onClick={() => handleRemoveActivity(activity)}
className="ml-2 text-textStandard hover:text-textSubtle transition-colors"
>
×
</button>
</div>
))}
</div>
<div className="flex gap-3 mt-6">
<input
type="text"
value={newActivity}
onChange={(e) => setNewActivity(e.target.value)}
onKeyPress={(e) => e.key === 'Enter' && handleAddActivity()}
className="flex-1 px-4 py-3 bg-bgSubtle text-textStandard rounded-xl placeholder-textPlaceholder focus:outline-none focus:ring-2 focus:ring-borderProminent"
placeholder="Add new activity..."
/>
<button
onClick={handleAddActivity}
className="px-5 py-3 bg-bgAppInverse text-textProminentInverse rounded-xl hover:bg-bgStandardInverse transition-colors"
>
Add activity
</button>
</div>
</div>
</div>
);
case 'instructions':
return (
<div className="p-6 pt-10">
<button onClick={() => setActiveSection('none')} className="mb-6">
<Back className="w-6 h-6 text-iconProminent" />
</button>
<div className="py-2">
<Bars className="w-6 h-6 text-iconSubtle" />
</div>
<div className="mb-8 mt-6">
<h2 className="text-2xl font-medium mb-2 text-textProminent">Instructions</h2>
<p className="text-textSubtle">
Hidden instructions that will be passed to the provider to help direct and add
context to your responses.
</p>
</div>
<textarea
value={instructions}
onChange={(e) => setInstructions(e.target.value)}
className="w-full h-96 p-4 bg-bgSubtle text-textStandard rounded-xl resize-none focus:outline-none focus:ring-2 focus:ring-borderProminent"
placeholder="Enter instructions..."
/>
</div>
);
case 'extensions':
return (
<div className="p-6 pt-10">
<button onClick={() => setActiveSection('none')} className="mb-6">
<Back className="w-6 h-6 text-iconProminent" />
</button>
<div className="py-2">
<Bars className="w-6 h-6 text-iconSubtle" />
</div>
<div className="mb-8 mt-6">
<h2 className="text-2xl font-medium mb-2 text-textProminent">Extensions</h2>
<p className="text-textSubtle">
Choose which extensions will be available to your agent.
</p>
</div>
<div className="grid grid-cols-2 gap-4">
{availableExtensions.map((extension) => (
<button
key={extension.name}
className="p-4 border border-borderSubtle rounded-lg flex justify-between items-center w-full text-left hover:bg-bgSubtle bg-bgApp"
onClick={() => handleExtensionToggle(extension.name)}
>
<div>
<h3 className="font-medium text-textProminent">{extension.name}</h3>
<p className="text-sm text-textSubtle">
{extension.description || 'No description available'}
</p>
</div>
<div className="relative inline-block w-10 align-middle select-none">
<div
className={`w-10 h-6 rounded-full transition-colors duration-200 ease-in-out ${
selectedExtensions.includes(extension.name)
? 'bg-bgAppInverse'
: 'bg-borderSubtle'
}`}
>
<div
className={`w-6 h-6 rounded-full bg-bgApp border-2 transform transition-transform duration-200 ease-in-out ${
selectedExtensions.includes(extension.name)
? 'translate-x-4 border-bgAppInverse'
: 'translate-x-0 border-borderSubtle'
}`}
/>
</div>
</div>
</button>
))}
</div>
</div>
);
default:
return (
<div className="space-y-4 py-4">
<div>
<h2 className="text-lg font-medium mb-2 text-textProminent">Agent</h2>
<input
type="text"
value={title}
onChange={(e) => setTitle(e.target.value)}
className="w-full p-3 border border-borderSubtle rounded-lg bg-bgApp text-textStandard"
placeholder="Agent Name"
/>
</div>
<div>
<input
type="text"
value={description}
onChange={(e) => setDescription(e.target.value)}
className="w-full p-3 border border-borderSubtle rounded-lg bg-bgApp text-textStandard"
placeholder="Description"
/>
</div>
{/* Section buttons */}
<button
onClick={() => setActiveSection('activities')}
className="w-full flex items-start justify-between p-4 border border-borderSubtle rounded-lg bg-bgApp hover:bg-bgSubtle"
>
<div className="text-left">
<h3 className="font-medium text-textProminent">Activities</h3>
<p className="text-textSubtle text-sm">
Starting activities present in the home panel on a fresh goose session
</p>
</div>
<ChevronRight className="w-5 h-5 mt-1 text-iconSubtle" />
</button>
<button
onClick={() => setActiveSection('instructions')}
className="w-full flex items-start justify-between p-4 border border-borderSubtle rounded-lg bg-bgApp hover:bg-bgSubtle"
>
<div className="text-left">
<h3 className="font-medium text-textProminent">Instructions</h3>
<p className="text-textSubtle text-sm">
Starting activities present in the home panel on a fresh goose session
</p>
</div>
<ChevronRight className="w-5 h-5 mt-1 text-iconSubtle" />
</button>
<button
onClick={() => setActiveSection('extensions')}
className="w-full flex items-start justify-between p-4 border border-borderSubtle rounded-lg bg-bgApp hover:bg-bgSubtle"
>
<div className="text-left">
<h3 className="font-medium text-textProminent">Extensions</h3>
<p className="text-textSubtle text-sm">
Starting activities present in the home panel on a fresh goose session
</p>
</div>
<ChevronRight className="w-5 h-5 mt-1 text-iconSubtle" />
</button>
{/* Deep Link Display */}
<div className="w-full p-4 bg-bgSubtle rounded-lg flex items-center justify-between">
<code className="text-sm text-textSubtle truncate">{deeplink}</code>
<button onClick={() => navigator.clipboard.writeText(deeplink)} className="ml-2">
<Copy className="w-5 h-5 text-iconSubtle" />
</button>
</div>
{/* Action Buttons */}
<div className="flex flex-col space-y-2 pt-4">
<button
onClick={() => {
const updatedConfig = getCurrentConfig();
window.electron.createChatWindow(
undefined,
undefined,
undefined,
undefined,
updatedConfig,
undefined
);
}}
className="w-full p-3 bg-bgAppInverse text-textProminentInverse rounded-lg hover:bg-bgStandardInverse"
>
Open agent
</button>
<button
onClick={() => window.close()}
className="w-full p-3 text-textSubtle rounded-lg hover:bg-bgSubtle"
>
Cancel
</button>
</div>
</div>
);
}
};
return (
<div className="flex flex-col w-full h-screen bg-bgApp max-w-3xl mx-auto">
{activeSection === 'none' && (
<div className="flex flex-col items-center mb-6 px-6 pt-10">
<div className="w-16 h-16 bg-bgApp rounded-full flex items-center justify-center mb-4">
<Geese className="w-12 h-12 text-iconProminent" />
</div>
<h1 className="text-2xl font-medium text-center text-textProminent">
Create custom agent
</h1>
<p className="text-textSubtle text-center mt-2 text-sm">
Your custom agent can be shared with others
</p>
</div>
)}
<div className="flex-1 overflow-y-auto px-6">{renderSectionContent()}</div>
</div>
);
}

View File

@@ -2,20 +2,41 @@ import React from 'react';
import SplashPills from './SplashPills';
import GooseLogo from './GooseLogo';
export default function Splash({ append, activities = null }) {
interface SplashProps {
append: (text: string) => void;
activities: string[] | null;
title?: string;
}
export default function Splash({ append, activities, title }: SplashProps) {
return (
<div className="flex flex-col h-full">
{title && (
<div className="flex items-center px-4 py-2">
<span className="w-2 h-2 rounded-full bg-blockTeal mr-2" />
<span className="text-sm">
<span className="text-textSubtle">Agent</span>{' '}
<span className="text-textStandard">{title}</span>
</span>
</div>
)}
<div className="flex flex-col flex-1">
<div className="h-full flex flex-col pb-12">
<div className="p-8">
<div className="relative text-textStandard mb-12">
<div className="w-min animate-[flyin_2s_var(--spring-easing)_forwards]">
<div className="scale-150">
<GooseLogo />
</div>
</div>
</div>
<div className="flex">
<div>
<SplashPills append={append} activities={activities} />
</div>
</div>
</div>
</div>
</div>
);
}

View File

@@ -1,15 +1,23 @@
import React from 'react';
function truncateText(text: string, maxLength: number = 100): string {
if (text.length <= maxLength) return text;
return text.slice(0, maxLength) + '...';
}
function SplashPill({ content, append, className = '', longForm = '' }) {
const displayText = truncateText(content);
return (
<div
className={`px-4 py-2 text-sm text-center text-textSubtle dark:text-textStandard cursor-pointer border border-borderSubtle hover:bg-bgSubtle rounded-full transition-all duration-150 ${className}`}
className={`px-4 py-2 text-sm text-center text-textStandard cursor-pointer border border-borderSubtle hover:bg-bgSubtle rounded-full transition-all duration-150 ${className}`}
onClick={async () => {
// Use the longForm text if provided, otherwise use the content
// Always use the full text (longForm or original content) when clicked
await append(longForm || content);
}}
title={content.length > 100 ? content : undefined} // Show full text on hover if truncated
>
<div className="line-clamp-2">{content}</div>
<div className="whitespace-normal">{displayText}</div>
</div>
);
}

View File

@@ -0,0 +1,11 @@
import React from 'react';
export function Bars() {
return (
<svg width="64" height="64" viewBox="0 0 64 64" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="32" cy="32" r="32" fill="#101010" />
<rect x="16" y="23" width="32" height="8" rx="4" fill="white" />
<rect x="16" y="34" width="20" height="8" rx="4" fill="white" />
</svg>
);
}

View File

@@ -0,0 +1,29 @@
import React from 'react';
interface Props {
className?: string;
// eslint-disable-next-line
[key: string]: any; // This will allow any other SVG props to pass through
}
export function ChevronRight({ className = '', ...props }: Props) {
return (
<svg
className={className}
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M9 18L15 12L9 6"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
}

View File

@@ -0,0 +1,46 @@
import React from 'react';
interface Props {
// eslint-disable-next-line
[key: string]: any; // This will allow any other SVG props to pass through
}
export function Geese({ ...props }: Props) {
return (
<svg
width="35"
height="37"
viewBox="0 0 35 37"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<rect y="0.5" width="35" height="36" rx="14" fill="url(#paint0_linear_4363_9473)" />
<path
d="M23.7099 18.589L23.0213 18.0222C22.6469 17.714 22.3283 17.3438 22.0791 16.9279C21.7349 16.3532 21.2593 15.8683 20.6916 15.5129L20.4143 15.3515C20.3193 15.2854 20.253 15.1825 20.2436 15.0664C20.2375 14.9915 20.2555 14.9246 20.2973 14.8657C20.4417 14.6622 21.2277 13.8163 21.3644 13.7032C21.5406 13.5576 21.7368 13.4365 21.9189 13.2979C21.9448 13.2781 21.9708 13.2585 21.9963 13.2385C21.9972 13.2376 21.9985 13.2369 21.9993 13.2361C22.0577 13.19 22.1129 13.141 22.1567 13.0829C22.3702 12.8356 22.3659 12.6267 22.3659 12.6267C22.3659 12.6267 22.3661 12.6265 22.3662 12.6264C22.3435 12.5708 22.2618 12.3976 22.0957 12.3164C22.2426 12.3135 22.4083 12.3685 22.4835 12.4421C22.5738 12.3004 22.6322 12.2085 22.7231 12.054C22.7447 12.0173 22.7562 11.9494 22.7148 11.9115C22.7148 11.9115 22.7146 11.9115 22.7144 11.9115C22.7144 11.9115 22.7144 11.9113 22.7144 11.9111C22.7136 11.9103 22.7126 11.9097 22.7119 11.9089C22.7111 11.9081 22.7106 11.9072 22.7097 11.9063C22.7097 11.9063 22.7095 11.9063 22.7093 11.9063C22.7093 11.9063 22.7093 11.9061 22.7093 11.9059C22.7076 11.9042 22.7057 11.9029 22.7039 11.9014C22.7028 11.9003 22.7019 11.899 22.7009 11.8978C22.6998 11.8968 22.6985 11.8959 22.6974 11.8948C22.6957 11.8931 22.6946 11.891 22.6928 11.8895C22.6928 11.8895 22.6925 11.8894 22.6924 11.8895C22.6924 11.8895 22.6924 11.8893 22.6924 11.8891C22.6916 11.8883 22.6906 11.8877 22.6898 11.8869C22.689 11.8861 22.6885 11.8851 22.6877 11.8843C22.6877 11.8843 22.6874 11.8842 22.6873 11.8843C22.6873 11.8843 22.6873 11.8841 22.6873 11.8839C22.6493 11.8425 22.5814 11.854 22.5448 11.8757C22.3438 11.9939 22.1484 12.1211 21.9721 12.2327C21.9721 12.2327 21.7633 12.2283 21.516 12.4419C21.4577 12.4859 21.4088 12.541 21.3627 12.5993C21.3618 12.6002 21.3611 12.6014 21.3604 12.6023C21.3402 12.6278 21.3206 12.6538 21.301 12.6797C21.1622 12.8619 21.0412 13.0581 20.8956 13.2342C20.7826 13.3711 19.9367 14.1569 19.7332 14.3013C19.6743 14.3431 19.6074 14.3612 19.5324 14.355C19.4164 14.3457 19.3134 14.2794 19.2473 14.1844L19.0859 13.9071C18.7306 13.3391 18.2456 12.8637 17.6709 12.5195C17.255 12.2704 16.8849 11.9516 16.5767 11.5774L16.0098 10.8887C15.9818 10.8548 15.9283 10.8604 15.9083 10.8994C15.8438 11.025 15.7207 11.2901 15.6264 11.6466C15.6241 11.655 15.6261 11.6639 15.6317 11.6707C15.7489 11.8123 16.0203 12.1244 16.3436 12.3884C16.3661 12.4067 16.3477 12.443 16.3197 12.4353C16.0408 12.3593 15.7697 12.2373 15.5668 12.1334C15.5503 12.125 15.5302 12.1355 15.5278 12.1539C15.4963 12.4066 15.4882 12.6841 15.5215 12.9767C15.5225 12.9862 15.5285 12.9944 15.5374 12.9983C15.7687 13.0989 16.1387 13.245 16.5272 13.3379C16.5553 13.3447 16.5545 13.3852 16.526 13.3907C16.2235 13.4476 15.9068 13.4603 15.6493 13.4569C15.6313 13.4567 15.618 13.4738 15.6228 13.4912C15.6745 13.6737 15.7445 13.8594 15.8369 14.0463C15.875 14.1303 15.9166 14.2127 15.9611 14.2933C15.9688 14.3071 15.9835 14.3154 15.9993 14.3147C16.2118 14.3043 16.4687 14.2818 16.7269 14.2362C16.7709 14.2285 16.7945 14.2864 16.7574 14.3113C16.582 14.4285 16.395 14.5295 16.221 14.6126C16.1977 14.6238 16.1902 14.6535 16.2056 14.6743C16.3089 14.8143 16.4226 14.9468 16.546 15.0702C16.546 15.0702 17.1539 15.6964 17.1705 15.7455C17.5252 15.3822 18.1084 14.9796 18.7523 14.628C17.8899 15.3298 17.4091 15.8478 17.1671 16.1422L16.9984 16.3789C16.9108 16.5019 16.8347 16.6325 16.7711 16.7694C16.5584 17.2271 16.2078 18.1528 16.2078 18.1528C16.1809 18.2253 16.2028 18.2968 16.2494 18.3434C16.2505 18.3445 16.2516 18.3456 16.2528 18.3465C16.2539 18.3476 16.2549 18.3488 16.2559 18.3499C16.3027 18.3966 16.3741 18.4184 16.4466 18.3916C16.4466 18.3916 17.372 18.0409 17.8299 17.8282C17.9668 17.7646 18.0975 17.6885 18.2204 17.6009L18.4811 17.4151C18.6192 17.3168 18.8082 17.3324 18.9281 17.4523L19.5291 18.0533C19.6525 18.1768 19.785 18.2904 19.9251 18.3938C19.9459 18.409 19.9755 18.4016 19.9867 18.3783C20.07 18.2044 20.1709 18.0172 20.288 17.842C20.3129 17.8048 20.3709 17.8285 20.3631 17.8724C20.3174 18.1308 20.2952 18.3876 20.2847 18.6C20.2839 18.6157 20.2922 18.6306 20.306 18.6382C20.3865 18.6828 20.4689 18.7244 20.553 18.7625C20.7399 18.8548 20.9257 18.9249 21.1082 18.9765C21.1255 18.9813 21.1426 18.968 21.1424 18.95C21.1389 18.6925 21.1516 18.3757 21.2086 18.0733C21.214 18.0448 21.2545 18.0439 21.2614 18.0721C21.3542 18.4607 21.5003 18.8307 21.601 19.062C21.6049 19.0708 21.613 19.0767 21.6227 19.0778C21.9152 19.1112 22.1927 19.103 22.4454 19.0715C22.4639 19.0692 22.4744 19.0492 22.4659 19.0325C22.362 18.8296 22.24 18.5583 22.164 18.2796C22.1564 18.2515 22.1926 18.2332 22.2109 18.2557C22.4749 18.579 22.787 18.8504 22.9286 18.9677C22.9354 18.9732 22.9444 18.9751 22.9527 18.9729C23.3094 18.8787 23.5744 18.7556 23.6999 18.6911C23.739 18.671 23.7445 18.6175 23.7106 18.5895L23.7099 18.589Z"
fill="white"
/>
<path
d="M26.8923 26.8445L26.2036 26.2776C25.8293 25.9695 25.5106 25.5993 25.2615 25.1833C24.9173 24.6087 24.4417 24.1237 23.8739 23.7684L23.5966 23.6069C23.5016 23.5409 23.4354 23.4379 23.426 23.3218C23.4199 23.2469 23.4379 23.18 23.4797 23.1211C23.6241 22.9176 24.41 22.0718 24.5468 21.9586C24.7229 21.813 24.9192 21.6919 25.1013 21.5533C25.1271 21.5336 25.1531 21.5139 25.1787 21.4939C25.1796 21.493 25.1809 21.4923 25.1817 21.4915C25.2401 21.4454 25.2952 21.3964 25.3391 21.3383C25.5526 21.0911 25.5483 20.8821 25.5483 20.8821C25.5483 20.8821 25.5485 20.8819 25.5486 20.8818C25.5259 20.8262 25.4442 20.653 25.2781 20.5718C25.425 20.5689 25.5907 20.624 25.6658 20.6976C25.7562 20.5558 25.8146 20.4639 25.9054 20.3094C25.9271 20.2727 25.9386 20.2049 25.8972 20.1669C25.8972 20.1669 25.897 20.1669 25.8968 20.1669C25.8968 20.1669 25.8968 20.1667 25.8968 20.1665C25.896 20.1657 25.895 20.1651 25.8942 20.1643C25.8934 20.1635 25.8929 20.1626 25.892 20.1617C25.892 20.1617 25.8918 20.1617 25.8916 20.1617C25.8916 20.1617 25.8916 20.1615 25.8916 20.1613C25.89 20.1597 25.8881 20.1584 25.8863 20.1568C25.8852 20.1557 25.8843 20.1544 25.8833 20.1532C25.8821 20.1522 25.8808 20.1513 25.8797 20.1502C25.8781 20.1486 25.877 20.1465 25.8752 20.1449C25.8752 20.1449 25.8749 20.1448 25.8748 20.1449C25.8748 20.1449 25.8748 20.1447 25.8748 20.1445C25.874 20.1437 25.873 20.1431 25.8722 20.1423C25.8714 20.1415 25.8708 20.1405 25.87 20.1397C25.87 20.1397 25.8697 20.1396 25.8696 20.1397C25.8696 20.1397 25.8696 20.1395 25.8696 20.1393C25.8317 20.098 25.7638 20.1095 25.7271 20.1311C25.5262 20.2493 25.3307 20.3765 25.1545 20.4881C25.1545 20.4881 24.9457 20.4837 24.6983 20.6974C24.6401 20.7413 24.5911 20.7964 24.5451 20.8547C24.5442 20.8556 24.5434 20.8568 24.5427 20.8577C24.5226 20.8832 24.503 20.9092 24.4833 20.9352C24.3446 21.1174 24.2236 21.3135 24.078 21.4896C23.965 21.6265 23.119 22.4123 22.9155 22.5567C22.8566 22.5986 22.7898 22.6166 22.7148 22.6105C22.5988 22.6011 22.4958 22.5348 22.4297 22.4398L22.2683 22.1625C21.9129 21.5946 21.428 21.1191 20.8533 20.7749C20.4374 20.5258 20.0673 20.2071 19.759 19.8328L19.1922 19.1442C19.1642 19.1102 19.1107 19.1158 19.0906 19.1549C19.0262 19.2804 18.9031 19.5455 18.8088 19.902C18.8065 19.9104 18.8085 19.9194 18.814 19.9261C18.9313 20.0677 19.2027 20.3798 19.526 20.6438C19.5485 20.6622 19.5301 20.6985 19.5021 20.6907C19.2232 20.6148 18.9521 20.4928 18.7492 20.3888C18.7326 20.3804 18.7126 20.3909 18.7102 20.4094C18.6787 20.6621 18.6705 20.9395 18.7039 21.2321C18.7049 21.2416 18.7109 21.2498 18.7197 21.2537C18.9511 21.3544 19.3211 21.5004 19.7096 21.5934C19.7377 21.6001 19.7368 21.6407 19.7084 21.6461C19.4059 21.7031 19.0891 21.7157 18.8317 21.7124C18.8136 21.7122 18.8004 21.7292 18.8052 21.7466C18.8569 21.9292 18.9269 22.1148 19.0192 22.3017C19.0574 22.3857 19.099 22.4682 19.1435 22.5488C19.1511 22.5626 19.1659 22.5708 19.1817 22.5701C19.3942 22.5597 19.651 22.5373 19.9093 22.4917C19.9533 22.4839 19.9769 22.5418 19.9397 22.5667C19.7644 22.6839 19.5774 22.7849 19.4034 22.868C19.3801 22.8792 19.3726 22.909 19.3879 22.9297C19.4913 23.0697 19.6049 23.2022 19.7284 23.3257C19.7284 23.3257 20.3363 23.9518 20.3529 24.0009C20.7076 23.6377 21.2907 23.235 21.9346 22.8835C21.0723 23.5852 20.5915 24.1033 20.3495 24.3976L20.1808 24.6343C20.0931 24.7573 20.0171 24.8879 19.9535 25.0249C19.7408 25.4825 19.3901 26.4082 19.3901 26.4082C19.3633 26.4807 19.3852 26.5522 19.4318 26.5988C19.4329 26.5999 19.434 26.601 19.4351 26.602C19.4362 26.6031 19.4372 26.6043 19.4383 26.6054C19.485 26.6521 19.5564 26.6739 19.6289 26.647C19.6289 26.647 20.5544 26.2963 21.0123 26.0836C21.1492 26.02 21.2799 25.9439 21.4028 25.8563L21.6635 25.6706C21.8016 25.5722 21.9906 25.5879 22.1105 25.7078L22.7114 26.3087C22.8349 26.4322 22.9674 26.5458 23.1074 26.6492C23.1283 26.6645 23.1579 26.657 23.1691 26.6337C23.2523 26.4599 23.3533 26.2726 23.4704 26.0974C23.4953 26.0602 23.5533 26.0839 23.5455 26.1278C23.4998 26.3862 23.4775 26.643 23.467 26.8555C23.4662 26.8711 23.4746 26.886 23.4884 26.8936C23.5689 26.9383 23.6513 26.9798 23.7354 27.0179C23.9223 27.1102 24.1081 27.1803 24.2905 27.2319C24.3079 27.2368 24.325 27.2235 24.3248 27.2054C24.3213 26.9479 24.334 26.6311 24.391 26.3288C24.3964 26.3002 24.4369 26.2993 24.4438 26.3276C24.5366 26.7161 24.6827 27.0861 24.7834 27.3174C24.7873 27.3262 24.7954 27.3322 24.805 27.3333C25.0976 27.3666 25.3751 27.3585 25.6278 27.3269C25.6463 27.3246 25.6568 27.3046 25.6483 27.2879C25.5444 27.085 25.4224 26.8137 25.3464 26.535C25.3388 26.507 25.375 26.4886 25.3933 26.5111C25.6573 26.8344 25.9694 27.1059 26.111 27.2231C26.1178 27.2286 26.1268 27.2305 26.1351 27.2283C26.4917 27.1341 26.7568 27.0111 26.8823 26.9465C26.9213 26.9265 26.9269 26.8729 26.893 26.8449L26.8923 26.8445Z"
fill="white"
/>
<path
d="M14.8377 19.8342L14.3546 19.4365C14.092 19.2204 13.8684 18.9607 13.6937 18.6689C13.4522 18.2658 13.1185 17.9256 12.7203 17.6763L12.5257 17.563C12.4591 17.5167 12.4126 17.4445 12.406 17.363C12.4018 17.3105 12.4144 17.2635 12.4437 17.2222C12.545 17.0795 13.0964 16.4861 13.1923 16.4067C13.3158 16.3046 13.4535 16.2197 13.5813 16.1224C13.5994 16.1086 13.6176 16.0948 13.6356 16.0807C13.6362 16.0801 13.6371 16.0796 13.6377 16.0791C13.6787 16.0467 13.7173 16.0123 13.7481 15.9716C13.8979 15.7981 13.8949 15.6516 13.8949 15.6516C13.8949 15.6516 13.895 15.6514 13.8951 15.6513C13.8791 15.6123 13.8218 15.4908 13.7053 15.4339C13.8083 15.4319 13.9246 15.4705 13.9773 15.5221C14.0407 15.4227 14.0817 15.3582 14.1454 15.2498C14.1606 15.2241 14.1686 15.1765 14.1396 15.1498C14.1396 15.1498 14.1395 15.1498 14.1393 15.1498C14.1393 15.1498 14.1393 15.1497 14.1393 15.1495C14.1388 15.149 14.1381 15.1486 14.1375 15.148C14.137 15.1475 14.1366 15.1468 14.136 15.1462C14.136 15.1462 14.1359 15.1462 14.1357 15.1462C14.1357 15.1462 14.1357 15.1461 14.1357 15.1459C14.1345 15.1447 14.1332 15.1438 14.132 15.1427C14.1312 15.142 14.1306 15.1411 14.1299 15.1402C14.129 15.1395 14.1281 15.1389 14.1274 15.1381C14.1262 15.137 14.1254 15.1355 14.1242 15.1344C14.1242 15.1344 14.124 15.1343 14.1239 15.1344C14.1239 15.1344 14.1239 15.1342 14.1239 15.1341C14.1233 15.1335 14.1226 15.1331 14.1221 15.1326C14.1215 15.132 14.1211 15.1313 14.1206 15.1308C14.1206 15.1308 14.1203 15.1307 14.1203 15.1308C14.1203 15.1308 14.1203 15.1306 14.1203 15.1305C14.0936 15.1015 14.046 15.1095 14.0203 15.1247C13.8794 15.2076 13.7422 15.2969 13.6186 15.3752C13.6186 15.3752 13.4721 15.3721 13.2986 15.5219C13.2578 15.5528 13.2234 15.5914 13.1911 15.6324C13.1905 15.633 13.1899 15.6338 13.1894 15.6344C13.1753 15.6523 13.1615 15.6705 13.1478 15.6888C13.0504 15.8166 12.9656 15.9542 12.8634 16.0777C12.7841 16.1737 12.1907 16.725 12.0479 16.8263C12.0066 16.8557 11.9597 16.8683 11.9071 16.864C11.8257 16.8575 11.7535 16.8109 11.7071 16.7443L11.5939 16.5498C11.3446 16.1513 11.0044 15.8178 10.6013 15.5763C10.3095 15.4016 10.0498 15.178 9.83363 14.9154L9.43597 14.4324C9.41635 14.4086 9.37878 14.4125 9.36473 14.4399C9.31951 14.5279 9.23318 14.7139 9.16702 14.964C9.16542 14.9699 9.16681 14.9762 9.1707 14.9809C9.25293 15.0803 9.44334 15.2992 9.67014 15.4844C9.68593 15.4972 9.67299 15.5227 9.65337 15.5173C9.45775 15.464 9.26754 15.3784 9.1252 15.3055C9.11359 15.2996 9.09953 15.307 9.09786 15.3199C9.07574 15.4972 9.07004 15.6918 9.09341 15.8971C9.09411 15.9037 9.09835 15.9095 9.10454 15.9122C9.26685 15.9828 9.52641 16.0853 9.79891 16.1505C9.81867 16.1552 9.81804 16.1837 9.79808 16.1875C9.58589 16.2274 9.36369 16.2364 9.18309 16.234C9.17042 16.2338 9.1611 16.2458 9.16451 16.258C9.20076 16.3861 9.24987 16.5163 9.31464 16.6474C9.34143 16.7064 9.37058 16.7642 9.40181 16.8207C9.40717 16.8304 9.41753 16.8362 9.4286 16.8357C9.57768 16.8284 9.75787 16.8127 9.93903 16.7807C9.96991 16.7752 9.98647 16.8159 9.96038 16.8333C9.83738 16.9156 9.70618 16.9864 9.58415 17.0447C9.5678 17.0525 9.56252 17.0734 9.5733 17.088C9.64579 17.1862 9.72552 17.2791 9.81213 17.3657C9.81213 17.3657 10.2386 17.805 10.2502 17.8394C10.4991 17.5846 10.9081 17.3022 11.3598 17.0555C10.7549 17.5478 10.4176 17.9112 10.2478 18.1177L10.1295 18.2838C10.068 18.37 10.0146 18.4617 9.97005 18.5577C9.82083 18.8788 9.57483 19.5282 9.57483 19.5282C9.55598 19.579 9.57135 19.6292 9.60405 19.6619C9.60481 19.6626 9.60558 19.6634 9.60641 19.6641C9.60718 19.6649 9.60788 19.6657 9.60864 19.6665C9.64141 19.6992 9.6915 19.7145 9.74235 19.6957C9.74235 19.6957 10.3916 19.4497 10.7128 19.3005C10.8088 19.2559 10.9005 19.2024 10.9867 19.141L11.1696 19.0107C11.2665 18.9417 11.3991 18.9527 11.4832 19.0368L11.9048 19.4584C11.9914 19.545 12.0843 19.6247 12.1826 19.6972C12.1972 19.7079 12.218 19.7027 12.2258 19.6864C12.2842 19.5644 12.355 19.4331 12.4372 19.3101C12.4546 19.284 12.4953 19.3007 12.4899 19.3315C12.4578 19.5127 12.4422 19.6929 12.4348 19.8419C12.4343 19.8529 12.4401 19.8633 12.4498 19.8687C12.5063 19.9 12.5641 19.9292 12.6231 19.9559C12.7542 20.0206 12.8845 20.0698 13.0125 20.106C13.0247 20.1094 13.0367 20.1001 13.0365 20.0874C13.0341 19.9068 13.043 19.6846 13.083 19.4724C13.0868 19.4524 13.1152 19.4518 13.12 19.4716C13.1851 19.7442 13.2876 20.0037 13.3583 20.166C13.361 20.1722 13.3667 20.1763 13.3734 20.1771C13.5787 20.2005 13.7733 20.1948 13.9506 20.1727C13.9636 20.1711 13.971 20.157 13.965 20.1453C13.8921 20.003 13.8065 19.8126 13.7532 19.6171C13.7479 19.5975 13.7733 19.5846 13.7861 19.6004C13.9713 19.8272 14.1903 20.0176 14.2896 20.0998C14.2943 20.1037 14.3007 20.105 14.3065 20.1035C14.5567 20.0374 14.7426 19.9511 14.8306 19.9058C14.8581 19.8917 14.862 19.8542 14.8382 19.8345L14.8377 19.8342Z"
fill="white"
/>
<defs>
<linearGradient
id="paint0_linear_4363_9473"
x1="17.5"
y1="0.5"
x2="17.5"
y2="36.5"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#595959" />
<stop offset="1" />
</linearGradient>
</defs>
</svg>
);
}

View File

@@ -6,6 +6,7 @@ import ChatSmart from './ChatSmart';
import Check from './Check';
import ChevronDown from './ChevronDown';
import ChevronUp from './ChevronUp';
import { ChevronRight } from './ChevronRight';
import Close from './Close';
import Copy from './Copy';
import Document from './Document';
@@ -28,6 +29,7 @@ export {
ChatSmart,
Check,
ChevronDown,
ChevronRight,
ChevronUp,
Close,
Copy,

View File

@@ -3,39 +3,39 @@ import { Buffer } from 'buffer';
import Copy from '../icons/Copy';
import { Card } from './card';
interface BotConfig {
interface RecipeConfig {
instructions?: string;
activities?: string[];
[key: string]: unknown;
}
interface DeepLinkModalProps {
botConfig: BotConfig;
recipeConfig: RecipeConfig;
onClose: () => void;
}
// Function to generate a deep link from a bot config
export function generateDeepLink(botConfig: BotConfig): string {
const configBase64 = Buffer.from(JSON.stringify(botConfig)).toString('base64');
export function generateDeepLink(recipeConfig: RecipeConfig): string {
const configBase64 = Buffer.from(JSON.stringify(recipeConfig)).toString('base64');
return `goose://bot?config=${configBase64}`;
}
export function DeepLinkModal({ botConfig: initialBotConfig, onClose }: DeepLinkModalProps) {
export function DeepLinkModal({ recipeConfig: initialRecipeConfig, onClose }: DeepLinkModalProps) {
// Create editable state for the bot config
const [botConfig, setBotConfig] = useState(initialBotConfig);
const [instructions, setInstructions] = useState(initialBotConfig.instructions || '');
const [activities, setActivities] = useState<string[]>(initialBotConfig.activities || []);
const [recipeConfig, setRecipeConfig] = useState(initialRecipeConfig);
const [instructions, setInstructions] = useState(initialRecipeConfig.instructions || '');
const [activities, setActivities] = useState<string[]>(initialRecipeConfig.activities || []);
const [activityInput, setActivityInput] = useState('');
// Generate the deep link using the current bot config
const deepLink = useMemo(() => {
const currentConfig = {
...botConfig,
...recipeConfig,
instructions,
activities,
};
return generateDeepLink(currentConfig);
}, [botConfig, instructions, activities]);
}, [recipeConfig, instructions, activities]);
// Handle Esc key press
useEffect(() => {
@@ -56,8 +56,8 @@ export function DeepLinkModal({ botConfig: initialBotConfig, onClose }: DeepLink
// Update the bot config when instructions or activities change
useEffect(() => {
setBotConfig({
...botConfig,
setRecipeConfig({
...recipeConfig,
instructions,
activities,
});
@@ -136,7 +136,7 @@ export function DeepLinkModal({ botConfig: initialBotConfig, onClose }: DeepLink
onClick={() => {
// Open the deep link with the current bot config
const currentConfig = {
...botConfig,
...recipeConfig,
instructions,
activities,
};

View File

@@ -62,6 +62,7 @@ export async function addExtension(
silent: boolean = false
): Promise<Response> {
try {
console.log('Adding extension:', extension);
// Create the config based on the extension type
const config = {
type: extension.type,
@@ -93,7 +94,29 @@ export async function addExtension(
body: JSON.stringify(config),
});
const data = await response.json();
const responseText = await response.text();
if (!response.ok) {
const errorMsg = `Server returned ${response.status}: ${response.statusText}. Response: ${responseText}`;
console.error(errorMsg);
if (toastId) toast.dismiss(toastId);
toastError({
title: extension.name,
msg: 'Failed to add extension',
traceback: errorMsg,
toastOptions: { autoClose: false },
});
return response;
}
// Only try to parse JSON if we got a successful response and have JSON content
let data;
try {
data = JSON.parse(responseText);
} catch (e) {
console.error('Failed to parse response as JSON:', e);
data = { error: true, message: responseText };
}
if (!data.error) {
if (!silent) {

View File

@@ -48,9 +48,34 @@ if (!gotTheLock) {
if (process.platform === 'win32') {
const protocolUrl = commandLine.find((arg) => arg.startsWith('goose://'));
if (protocolUrl) {
const parsedUrl = new URL(protocolUrl);
// If it's a bot/recipe URL, handle it directly by creating a new window
if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
app.whenReady().then(() => {
const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
let recipeConfig = null;
const configParam = parsedUrl.searchParams.get('config');
if (configParam) {
try {
recipeConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
} catch (e) {
console.error('Failed to parse bot config:', e);
}
}
createChat(app, undefined, openDir, undefined, undefined, recipeConfig);
});
return; // Skip the rest of the handler
}
// For non-bot URLs, continue with normal handling
handleProtocolUrl(protocolUrl);
}
// Only focus existing windows for non-bot/recipe URLs
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
const mainWindow = existingWindows[0];
@@ -61,7 +86,6 @@ if (!gotTheLock) {
}
}
});
if (process.platform === 'win32') {
const protocolUrl = process.argv.find((arg) => arg.startsWith('goose://'));
if (protocolUrl) {
@@ -79,12 +103,17 @@ async function handleProtocolUrl(url: string) {
if (!url) return;
pendingDeepLink = url;
const parsedUrl = new URL(url);
const parsedUrl = new URL(url);
const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
if (parsedUrl.hostname !== 'bot') {
if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
// For bot/recipe URLs, skip existing window processing
// and let processProtocolUrl handle it entirely
processProtocolUrl(parsedUrl, null);
} else {
// For other URL types, reuse existing window if available
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
firstOpenWindow = existingWindows[0];
@@ -95,7 +124,6 @@ async function handleProtocolUrl(url: string) {
} else {
firstOpenWindow = await createChat(app, undefined, openDir);
}
}
if (firstOpenWindow) {
const webContents = firstOpenWindow.webContents;
@@ -108,6 +136,7 @@ async function handleProtocolUrl(url: string) {
}
}
}
}
function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) {
const recentDirs = loadRecentDirs();
@@ -117,30 +146,48 @@ function processProtocolUrl(parsedUrl: URL, window: BrowserWindow) {
window.webContents.send('add-extension', pendingDeepLink);
} else if (parsedUrl.hostname === 'sessions') {
window.webContents.send('open-shared-session', pendingDeepLink);
} else if (parsedUrl.hostname === 'bot') {
let botConfig = null;
} else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
let recipeConfig = null;
const configParam = parsedUrl.searchParams.get('config');
if (configParam) {
try {
botConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
recipeConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
} catch (e) {
console.error('Failed to parse bot config:', e);
}
}
createChat(app, undefined, openDir, undefined, undefined, botConfig);
// Create a new window and ignore the passed-in window
createChat(app, undefined, openDir, undefined, undefined, recipeConfig);
}
pendingDeepLink = null;
}
app.on('open-url', async (event, url) => {
if (process.platform !== 'win32') {
pendingDeepLink = url;
const parsedUrl = new URL(url);
const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
if (parsedUrl.hostname !== 'bot') {
// Handle bot/recipe URLs by directly creating a new window
if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
let recipeConfig = null;
const configParam = parsedUrl.searchParams.get('config');
if (configParam) {
try {
recipeConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
} catch (e) {
console.error('Failed to parse bot config:', e);
}
}
// Create a new window directly
await createChat(app, undefined, openDir, undefined, undefined, recipeConfig);
return; // Skip the rest of the handler
}
// For non-bot URLs, continue with normal handling
pendingDeepLink = url;
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
firstOpenWindow = existingWindows[0];
@@ -149,23 +196,11 @@ app.on('open-url', async (event, url) => {
} else {
firstOpenWindow = await createChat(app, undefined, openDir);
}
}
if (parsedUrl.hostname === 'extension') {
firstOpenWindow.webContents.send('add-extension', pendingDeepLink);
} else if (parsedUrl.hostname === 'sessions') {
firstOpenWindow.webContents.send('open-shared-session', pendingDeepLink);
} else if (parsedUrl.hostname === 'bot') {
let botConfig = null;
const configParam = parsedUrl.searchParams.get('config');
if (configParam) {
try {
botConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
} catch (e) {
console.error('Failed to parse bot config:', e);
}
}
firstOpenWindow = await createChat(app, undefined, openDir, undefined, undefined, botConfig);
}
}
});
@@ -221,6 +256,7 @@ const getVersion = () => {
};
let [provider, model] = getGooseProvider();
console.log('[main] Got provider and model:', { provider, model });
let sharingUrl = getSharingUrl();
@@ -235,11 +271,13 @@ let appConfig = {
secretKey: generateSecretKey(),
};
console.log('[main] Created appConfig:', appConfig);
// Track windows by ID
let windowCounter = 0;
const windowMap = new Map<number, BrowserWindow>();
interface BotConfig {
interface RecipeConfig {
id: string;
name: string;
description: string;
@@ -253,12 +291,42 @@ const createChat = async (
dir?: string,
version?: string,
resumeSessionId?: string,
botConfig?: BotConfig
recipeConfig?: RecipeConfig, // Bot configuration
viewType?: string // View type
) => {
// Initialize variables for process and configuration
let port = 0;
let working_dir = '';
let goosedProcess = null;
if (viewType === 'recipeEditor') {
// For recipeEditor, get the port from existing windows' config
const existingWindows = BrowserWindow.getAllWindows();
if (existingWindows.length > 0) {
// Get the config from localStorage through an existing window
try {
const result = await existingWindows[0].webContents.executeJavaScript(
`localStorage.getItem('gooseConfig')`
);
if (result) {
const config = JSON.parse(result);
port = config.GOOSE_PORT;
working_dir = config.GOOSE_WORKING_DIR;
}
} catch (e) {
console.error('Failed to get config from localStorage:', e);
}
}
if (port === 0) {
console.error('No existing Goose process found for recipeEditor');
throw new Error('Cannot create recipeEditor window: No existing Goose process found');
}
} else {
// Apply current environment settings before creating chat
updateEnvironmentVariables(envToggles);
const [port, working_dir, goosedProcess] = await startGoosed(app, dir);
// Start new Goosed process for regular windows
[port, working_dir, goosedProcess] = await startGoosed(app, dir);
}
const mainWindow = new BrowserWindow({
titleBarStyle: process.platform === 'darwin' ? 'hidden' : 'default',
@@ -279,17 +347,37 @@ const createChat = async (
JSON.stringify({
...appConfig,
GOOSE_PORT: port,
GOOSE_WORKING_DIR: working_dir,
GOOSE_WORKGIN_DIR: working_dir,
REQUEST_DIR: dir,
GOOSE_BASE_URL_SHARE: sharingUrl,
GOOSE_VERSION: gooseVersion,
botConfig: botConfig,
recipeConfig: recipeConfig,
}),
],
partition: 'persist:goose', // Add this line to ensure persistence
},
});
// Store config in localStorage for future windows
const windowConfig = {
...appConfig,
GOOSE_PORT: port,
GOOSE_WORKING_DIR: working_dir,
REQUEST_DIR: dir,
GOOSE_BASE_URL_SHARE: sharingUrl,
recipeConfig: recipeConfig,
};
// We need to wait for the window to load before we can access localStorage
mainWindow.webContents.on('did-finish-load', () => {
const configStr = JSON.stringify(windowConfig).replace(/'/g, "\\'");
mainWindow.webContents.executeJavaScript(`
localStorage.setItem('gooseConfig', '${configStr}')
`);
});
console.log('[main] Creating window with config:', windowConfig);
// Handle new window creation for links
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
// Open all links in external browser
@@ -313,6 +401,13 @@ const createChat = async (
: `?resumeSessionId=${encodeURIComponent(resumeSessionId)}`;
}
// Add view type to query params if provided
if (viewType) {
queryParams = queryParams
? `${queryParams}&view=${encodeURIComponent(viewType)}`
: `?view=${encodeURIComponent(viewType)}`;
}
const primaryDisplay = electron.screen.getPrimaryDisplay();
const { width } = primaryDisplay.workAreaSize;
@@ -351,9 +446,12 @@ const createChat = async (
});
windowMap.set(windowId, mainWindow);
// Handle window closure
mainWindow.on('closed', () => {
windowMap.delete(windowId);
if (goosedProcess) {
goosedProcess.kill();
}
});
return mainWindow;
};
@@ -477,6 +575,10 @@ ipcMain.on('react-ready', () => {
} else {
console.log('No pending deep link to process');
}
// We don't need to handle pending deep links here anymore
// since we're handling them in the window creation flow
console.log('[main] React ready - window is prepared for deep links');
});
// Handle directory chooser
@@ -675,10 +777,10 @@ app.whenReady().then(async () => {
// Extract the bot config from the URL
const configParam = new URL(sqlBotUrl).searchParams.get('config');
let botConfig = null;
let recipeConfig = null;
if (configParam) {
try {
botConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
recipeConfig = JSON.parse(Buffer.from(configParam, 'base64').toString('utf-8'));
} catch (e) {
console.error('Failed to parse bot config:', e);
}
@@ -688,7 +790,7 @@ app.whenReady().then(async () => {
const recentDirs = loadRecentDirs();
const openDir = recentDirs.length > 0 ? recentDirs[0] : null;
createChat(app, undefined, openDir, undefined, undefined, botConfig);
createChat(app, undefined, openDir, undefined, undefined, recipeConfig);
},
})
);
@@ -704,13 +806,21 @@ app.whenReady().then(async () => {
}
});
ipcMain.on('create-chat-window', (_event, query, dir, version, resumeSessionId, botConfig) => {
ipcMain.on(
'create-chat-window',
(_, query, dir, version, resumeSessionId, recipeConfig, viewType) => {
if (!dir?.trim()) {
const recentDirs = loadRecentDirs();
dir = recentDirs.length > 0 ? recentDirs[0] : null;
}
createChat(app, query, dir, version, resumeSessionId, botConfig);
});
// Log the recipeConfig for debugging
console.log('Creating chat window with recipeConfig:', recipeConfig);
// Pass recipeConfig as part of viewOptions when viewType is recipeEditor
createChat(app, query, dir, version, resumeSessionId, recipeConfig, viewType);
}
);
ipcMain.on('notify', (_event, data) => {
console.log('NOTIFY', data);

View File

@@ -1,6 +1,6 @@
import Electron, { contextBridge, ipcRenderer } from 'electron';
interface BotConfig {
interface RecipeConfig {
id: string;
name: string;
description: string;
@@ -34,7 +34,8 @@ type ElectronAPI = {
dir?: string,
version?: string,
resumeSessionId?: string,
botConfig?: BotConfig
recipeConfig?: RecipeConfig,
viewType?: string
) => void;
logInfo: (txt: string) => void;
showNotification: (data: NotificationData) => void;
@@ -74,8 +75,18 @@ const electronAPI: ElectronAPI = {
dir?: string,
version?: string,
resumeSessionId?: string,
botConfig?: BotConfig
) => ipcRenderer.send('create-chat-window', query, dir, version, resumeSessionId, botConfig),
recipeConfig?: RecipeConfig,
viewType?: string
) =>
ipcRenderer.send(
'create-chat-window',
query,
dir,
version,
resumeSessionId,
recipeConfig,
viewType
),
logInfo: (txt: string) => ipcRenderer.send('logInfo', txt),
showNotification: (data: NotificationData) => ipcRenderer.send('notify', data),
openInChrome: (url: string) => ipcRenderer.send('open-in-chrome', url),

View File

@@ -0,0 +1,59 @@
import { Message } from '../types/message';
import { getApiUrl } from '../config';
import { FullExtensionConfig } from '../extensions';
export interface Recipe {
title: string;
description: string;
instructions: string;
activities?: string[];
author?: {
contact?: string;
metadata?: string;
};
extensions?: FullExtensionConfig[];
goosehints?: string;
context?: string[];
}
export interface CreateRecipeRequest {
messages: Message[];
title: string;
description: string;
activities?: string[];
author?: {
contact?: string;
metadata?: string;
};
}
export interface CreateRecipeResponse {
recipe: Recipe | null;
error: string | null;
}
export async function createRecipe(request: CreateRecipeRequest): Promise<CreateRecipeResponse> {
const url = getApiUrl('/recipe/create');
console.log('Creating recipe at:', url);
console.log('Request:', JSON.stringify(request, null, 2));
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(request),
});
if (!response.ok) {
const errorText = await response.text();
console.error('Failed to create recipe:', {
status: response.status,
statusText: response.statusText,
error: errorText,
});
throw new Error(`Failed to create recipe: ${response.statusText} (${errorText})`);
}
return response.json();
}

View File

@@ -173,9 +173,9 @@ export const initializeSystem = async (
console.log('Model synced with React state:', syncedModel);
}
// Get botConfig directly here
const botConfig = window.appConfig?.get?.('botConfig');
const botPrompt = botConfig?.instructions;
// Get recipeConfig directly here
const recipeConfig = window.appConfig?.get?.('recipeConfig');
const botPrompt = recipeConfig?.instructions;
// Extend the system prompt with desktop-specific information
const response = await fetch(getApiUrl('/agent/prompt'), {